Emblor

repository·main·Indexed 22 days ago

https://github.com/jaleelb/emblor

A highly customizable, accessible, and fully-featured tag input component built on top of Shadcn UI. Emblor provides the TagInput and Autocomplete components, supporting features such as drag-and-drop reordering, custom delimiters, tag limits, validation, and integration with react-hook-form and zod.

Tokens
9.8K
Snippets
19
Records
44
Agent score
77%

What's inside emblor

  1. Style the TagInput component using styleClasses

    main

    The TagInput component provides a styleClasses prop that accepts an object of custom class names. This allows you to apply fine-grained styling to various subcomponents (like the input field, containers, popovers, and individual tags) by targeting their specific keys. The structure of the styleClasses object follows the component's internal anatomy.

    <TagInput
      styleClasses={{
        input: 'border border-gray-300 p-2',
        inlineTagsContainer: 'bg-gray-200 p-2 rounded',
        tagPopover: {
          popoverContent: 'bg-white shadow-lg',
          popoverTrigger: 'text-blue-500 hover:text-blue-600',
        },
        tagList: {
          container: 'bg-red-100',
          sortableList: 'p-1',
        },
        autoComplete: {
          command: 'bg-blue-100',
          popoverTrigger: 'bg-green-200',
          popoverContent: 'p-4',
          commandList: 'list-none',
          commandGroup: 'font-bold',
          commandItem: 'cursor-pointer hover:bg-gray-100',
        },
        tag: {
          body: 'flex items-center gap-2',
          closeButton: 'text-red-500 hover:text-red-600',
        },
        clearAllButton: 'text-red-500 hover:text-red-600',
      }}
      // other props
    />
  2. Integrate Emblor with shadcn/ui Form

    main

    This guide demonstrates how to integrate Emblor's TagInput component into a shadcn/ui form, which utilizes react-hook-form and zod for validation and state management.

    Prerequisites

    • A project with shadcn/ui installed.
    • react-hook-form and zod installed.

    Installation

    1. Install the shadcn/ui Form component:
    npx shadcn-ui@latest add form
    1. Install Emblor:
    npm install emblor

    Implementation Steps

    1. Create a form schema

    Define the shape of your form using a Zod schema. For a tag input, the schema should expect an array of objects containing id and text.

    2. Define a form

    Use the useForm hook from react-hook-form with the zodResolver. Because FormField uses controlled components, you must provide defaultValues (e.g., an empty array for tags).

    3. Build your form

    Wrap your form in the <Form> provider and use <FormField> to render the TagInput. To sync the TagInput state with react-hook-form, use the setTags prop to call setValue from the form instance.

    Anatomy of the Integration

    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="topics"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Topics</FormLabel>
              <FormControl>
                <TagInput
                  {...field}
                  tags={tags}
                  setTags={(newTags) => {
                    setTags(newTags);
                    setValue('topics', newTags);
                  }}
                />
              </FormControl>
              <FormDescription>Description text</FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
    import { 1. Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
    import { Button } from '@/components/ui/button';
    import { z } from 'zod';
    import { useForm } from 'react-hook-form';
    import { zodResolver } from '@hookform/resolvers/zod';
    import React from 'react';
    import { Tag, TagInput } from 'emblor';
    
    const FormSchema = z.object({
      topics: z.array(
        z.object({
          id: z.string(),
          text: z.string(),
        }),
      ),
    });
    
    export default function Demo() {
      const form = useForm<z.infer<typeof FormSchema>>({
        resolver: zodResolver(FormSchema),
        defaultValues: {
          topics: [],
        },
      });
    
      const [tags, setTags] = React.useState<Tag[]>([]);
      const { setValue } = form;
    
      function onSubmit(data: z.infer<typeof FormSchema>) {
        console.log(data);
      }
    
      return (
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 flex flex-col items-start">
            <FormField
              control={form.control}
              name="topics"
              render={({ field }) => (
                <FormItem className="flex flex-col items-start">
                  <FormLabel className="text-left">Topics</FormLabel>
                  <FormControl className="w-full">
                    <TagInput
                      {...field}
                      placeholder="Enter a topic"
                      tags={tags}
                      className="sm:min-w-[450px]"
                      setTags={(newTags) => {
                        setTags(newTags);
                        setValue('topics', newTags as [Tag, ...Tag[]]);
                      }}
                    />
                  </FormControl>
                  <FormDescription className="text-left">
                    These are the topics that you&apos;re interested in.
                  </FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />
            <Button type="submit">Submit</Button>
          </form>
        </Form>
      );
    }
  3. Manage active tag state for keyboard navigation

    main

    To enable keyboard navigation (using Arrow keys, Home, End, etc.) in the TagInput component, you must manage the active tag state externally. You need to provide both activeTagIndex (a number or null) and setActiveTagIndex (a state setter function) as props to the component. This allows the component to communicate which tag is currently focused/active to your parent component.

    import {TagInput} from 'emblor';
    
    const [tags, setTags] = React.useState<Tag[]>([]);
    const [activeTagIndex, setActiveTagIndex] = React.useState<number | null>(null);
    
    <TagInput
      {...field}
      placeholder="Enter a topic"
      tags={tags}
      setTags={(newTags) => {
        setTags(newTags);
        setValue('topics', newTags as [Tag, ...Tag[]]);
      }}
      activeTagIndex={activeTagIndex}
      setActiveTagIndex={setActiveTagIndex}
    />;
  4. Integrate Emblor TagInput with React Hook Form

    main

    To integrate Emblor's TagInput with react-hook-form, use the Controller component. This allows you to bridge the TagInput state with the form's internal state management.

    Key Steps:

    1. Use the Controller component from react-hook-form.
    2. Pass the control object from useForm() to the Controller.
    3. In the render prop of the Controller, spread the field object onto the TagInput component ({...field}).
    4. Use the setTags prop of TagInput to update both your local component state and the form state using setValue from useForm().

    Note: When calling setValue, you may need to cast the tags to the expected type (e.g., newTags as [Tag, ...Tag[]]) to satisfy TypeScript requirements for non-empty arrays if your schema requires it.

    import { useForm, Controller } from 'react-hook-form';
    import { Tag, TagInput } from 'emblor';
    
    function TagForm() {
      const { control, handleSubmit, setValue } = useForm();
      const [tags, setTags] = React.useState<Tag[]>([]);
    
      const onSubmit = (data) => {
        console.log(data.tags); // Process tag data
      };
    
      return (
        <form onSubmit={handleSubmit(onSubmit)}>
          <Controller
            name="tags"
            control={control}
            render={({ field }) => (
              <TagInput
                {...field}
                placeholder="Enter a topic"
                tags={tags}
                className="sm:min-w-[450px]"
                setTags={(newTags) => {
                  setTags(newTags);
                  setValue('topics', newTags as [Tag, ...Tag[]]);
                }}
              />
            )}
          />
          <button type="submit">Submit</button>
        </form>
      );
    }
    
    export default TagForm;
  5. Configure TagInput autocomplete

    main

    To enable autocomplete suggestions, set enableAutocomplete={true} and provide an array of Tag objects to autocompleteOptions.

    Additional controls:

    • restrictTagsToAutocompleteOptions: If true, users can only add tags that exist in the autocompleteOptions list.
    • autocompleteFilter: A function (option: string) => boolean to customize which options are shown.
    • usePortal: If true, the autocomplete popover is rendered in a React Portal.
  6. Implement TagInput with React Hook Form

    main

    The following example demonstrates how to integrate TagInput into a form using react-hook-form and zod. Note that you must manage the tags state manually and update your form value via setTags and the form's setValue method.

    import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
    import { Tag, TagInput } from 'emblor';
    import { z } from 'zod';
    import { useForm } from 'react-hook-form';
    import { zodResolver } from '@hookform/resolvers/zod';
    import React from 'react';
    
    const FormSchema = z.object({
      topics: z.array(
        z.object({
          id: z.string(),
          text: z.string(),
        }),
      ),
    });
    
    export default function Hero() {
      const form = useForm<z.infer<typeof FormSchema>>({
        resolver: zodResolver(FormSchema),
      });
    
      const [tags, setTags] = React.useState<Tag[]>([]);
      const { setValue } = form;
    
      return (
        <FormField
          control={form.control}
          name="topics"
          render={({ field }) => (
            <FormItem className="flex flex-col items-start">
              <FormLabel className="text-left">Topics</FormLabel>
              <FormControl>
                <TagInput
                  {...field}
                  placeholder="Enter a topic"
                  tags={tags}
                  setTags={(newTags) => {
                    setTags(newTags);
                    setValue('topics', newTags as [Tag, ...Tag[]]);
                  }}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
      );
    }
  7. Use the TagInput component

    main

    To integrate Emblor into your React application, import the TagInput component. You typically manage the tags state and an activeTagIndex state to control the component's behavior and value.

    import { TagInput } from 'emblor';
    
    const [tags, setTags] = React.useState<Tag[]>([]);
    const [activeTagIndex, setActiveTagIndex] = React.useState<number | null>(null);
    
    <TagInput
      {...field}
      placeholder="Enter a topic"
      tags={tags}
      setTags={(newTags) => {
        setTags(newTags);
        setValue('topics', newTags as [Tag, ...Tag[]]);
      }}
      activeTagIndex={activeTagIndex}
      setActiveTagIndex={setActiveTagIndex}
    />;