i-like-robots / react-tag-autocomplete

⚛️ A simple, accessible, tagging component ready to drop into your React projects (new repo)
https://i-like-robots.github.io/react-tag-autocomplete/
ISC License
168 stars 11 forks source link

Ability to add new tags automatically when pasting text on the input #73

Open mercera opened 1 month ago

mercera commented 1 month ago

As mentioned in the title it would be nice to have an option to add tags automatically when pasting on the input. Current behaviour is to paste the text and press enter or click on add "tag name" which appears in the suggestion list to add the new tag. I have tried to use onInput prop to achieve this functionality but the callback function does not seem to return the event object, Therefore there is no way to know if the input is from pasting or just from typing. :)

ellunium commented 1 month ago

One way you can achieve that is by using the renderInput prop.

Probably over engineered, but here is an example of how you could achieve it.

I hope that helps.

import React, { useCallback, useRef, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import {
  InputRendererProps,
  ReactTags,
  ReactTagsAPI,
  TagSelected,
} from "react-tag-autocomplete"; // Importing components and types from 'react-tag-autocomplete'
import { suggestions } from "./countries";
import "./styles.css";

export default function App() {
  const api = useRef<ReactTagsAPI>(null); // Ref to hold the instance of ReactTagsAPI
  const [selected, setSelected] = useState<TagSelected[]>([]); // State to manage the selected tags

  // Function to validate if a new tag can be added
  const onValidate = (value: string) => {
    return (
      selected.filter(
        (tag) => tag.label.toLocaleLowerCase() === value.toLocaleLowerCase()
      ).length === 0
    );
  };

  // Function to add a new tag to the list
  const onAdd = useCallback(
    (newTag: TagSelected) => {
      setSelected([...selected, newTag]);
    },
    [selected] // Dependency array ensures onAdd function has the latest state
  );

  // Function to remove a tag from the list
  const onDelete = useCallback(
    (tagIndex: number) => {
      setSelected(selected.filter((_, i) => i !== tagIndex));
    },
    [selected] // Dependency array ensures onDelete function has the latest state
  );

  // Function to handle paste events and add tags from clipboard data
  const onPaste = useCallback(
    (e: React.ClipboardEvent<HTMLInputElement>) => {
      const event = e;
      const clipboardData = event.clipboardData;
      const pastedText = clipboardData?.getData("text").trim() || "";
      const existingTagSuggestion = suggestions.find(
        (suggestion) =>
          suggestion.label.toLocaleLowerCase() ===
          pastedText.toLocaleLowerCase()
      );

      if (onValidate(pastedText)) {
        existingTagSuggestion
          ? setSelected([...selected, existingTagSuggestion])
          : setSelected([...selected, { value: uuidv4(), label: pastedText }]);

        // Clear the input field after pasting.
        // There is probably a better way to do this.
        setTimeout(() => {
          if (api?.current?.input?.value) {
            api.current.input.value = "";
          }
        }, 5);
      }
    },
    [selected] // Dependency array ensures onDelete function has the latest state
  );

  // Custom input component for rendering the input field
  function customInput({
    classNames,
    inputWidth,
    ...inputProps
  }: InputRendererProps) {
    return (
      <input
        className={classNames.input}
        style={{ width: inputWidth }}
        onPaste={onPaste} // Attach custom paste handler
        {...inputProps} // Spread other input properties
      />
    );
  }

  return (
    <ReactTags
      ref={api} // Set the ref to ReactTagsAPI instance
      allowNew // Allow new tags that are not in suggestions
      activateFirstOption // Automatically activate the first suggestion
      labelText="Select tags" // Label for the input field
      selected={selected} // Array of selected tags
      suggestions={suggestions} // Array of suggestion tags
      onAdd={onAdd} // Handler to add a tag
      onDelete={onDelete} // Handler to delete a tag
      onValidate={onValidate} // Handler to validate a new tag
      renderInput={customInput} // Custom input rendering
      noOptionsText="No matching tags" // Text to show when no options match
    />
  );
}
mercera commented 1 month ago

Thanks 👍. Will try this out.

i-like-robots commented 1 month ago

Thanks for your question @mercera and your detailed response @ellunium.

There have been multiple discussions about adding similar behaviour to earlier versions of this component, however each time we found that the expected behaviour would change depending on the context, so no consensus was ever reached. Perhaps we could add an onPaste callback to at least make it a little easier to add custom logic?

mercera commented 1 month ago

I have tried out @ellunium's solution and it's working perfectly. Thanks a lot btw :). @i-like-robots yes. I like that idea as well since that would make things much easier.