😸

TailwindCSSとReactでタグ付けするInputを実装する

2023/10/23に公開

ReactTagsInput

React上でフォームのInputに入力したものをタグとして表示させるUIコンポートを実装する必要があったのでそのメモになります。
簡易実装はこちらのCodePenを参考にしてください。

https://codepen.io/ta_kasy/pen/mdvdaLw

環境

  • React
  • TailwindCSS
  • clsx
  • TypeScript

実装

Propsの定義

TagsInput.tsx
type TagsInputProps = React.ComponentPropsWithoutRef<'input'> & {
    isError?: boolean
    tags: string[]
    onChangeTags?: (tags: string[]) => void
}

isErrorはフォーム入力に誤りがある場合、赤枠を表示するためのものです。
tagsが入力されたタグ一覧。
onChangeTagsはタグ一覧に変化があった場合、呼び出されるCallbackです。
Callbackが実行されるタイミングは以下になります。

  • 文字列を入力し、Enterををされた場合、タグ追加
  • タグのバツボタンが押された場合、タグ削除
  • Input未入力時にBackspaceが押された場合のタグ削除

UIの作成

Badgeコンポーネント

Badge.tsx
import React from 'react'
import clsx from 'clsx'

const variants = {
  primary: 'bg-green-500 text-white',
  inverse: 'bg-white text-gray-600 border',
  danger: 'bg-red-500 text-white'
}

const sizes = {
  sm: 'py-1 px-1 text-sm',
  md: 'py-1 px-2 text-md',
  lg: 'py-2 px-3 text-lg'
}

type BadgeProps = React.ComponentPropsWithoutRef<'span'> & {
  variant?: keyof typeof variants
  size?: keyof typeof sizes
  onClose?: () => void
}

export const Badge: React.FC<BadgeProps> = ({ variant = 'primary', size = 'md', onClose, className, ...props }) => {
  return (
    <span className={clsx('font-medium rounded inline-flex items-center', variants[variant], className)}>
      <span className={clsx(sizes[size])}>{props.children}</span>
      {onClose && (
        <span
          className={clsx('inline-flex items-center border-l h-full w-hull cursor-pointer', sizes[size])}
          onClick={() => onClose && onClose()}
        >
          x
        </span>
      )}
    </span>
  )
}

TagsInputコンポーネント

const styles = {
  default: 'border-gray-200 focus:bg-white focus:border-gray-500',
  error: 'border-red-500 focus:bg-white focus:border-gray-500'
}

/* eslint @typescript-eslint/no-unused-vars: 0 */
/* eslint react/prop-types: 0 */
export const TagsInput: React.FC<TagsInputProps> = ({ onChangeTags, tags = [], isError, className, ...props }) => {
  return (
    <div
      className={clsx(
        'flex flex-wrap text-gray-700 border leading-tight pt-3 pb-2 px-4 rounded',
        styles[isError ? 'error' : 'default']
      )}
    >
      {tags.map((tag, i) => {
        return (
          <Badge key={i} size="sm" className={'mr-1 mb-1'} onClose={() => onclose(i)}>
            {tag}
          </Badge>
        )
      })}
      <input
        type="text"
        className={'flex-grow border-0 mb-1 outline-none'}
        {...props}
      />
    </div>
  )
}

入力時の動作を実装

inputタグのonKeyDownの入力を監視するCallbackを実装します。
isComposingとは日本語入力などで変換中で入力が確定していない場合falseが入ってきます。
確定した文字列だけを処理するためisComposing=falseの場合は処理をスキップしています。

https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/isComposing

入力文字がBackspaceかつinput入力欄が空の場合はTagの削除を実行し、Enterが押された場合はタグリストに追加する処理を実装しています。
最後にe.preventDefault()を記入しているのはinput上でEnterを押した場合、フォームの送信イベントが発火しないようにするためです。

TagsInput.tsx
const onClose = (i: number) => {
    const newTags = [...tags]
    newTags.splice(i, 1)
    oChangeTags && onChangeTags(newTags)
}

function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.nativeEvent.isComposing) return

    const value = e.currentTarget.value
    if (e.key === 'Backspace' && !value.length && tags.length > 0) {
        onClose(tags.length - 1)
        return
    }

    if (e.key !== 'Enter' || !value.trim()) return
    const newTags = [...tags, value]
    onChangeTags && onChangeTags(newTags)
    e.currentTarget.value = ''
    e.preventDefault()
}

使用方法

const App: React.FC = () => {
  const [tags, setTags] = React.useState(["タグ1", "タグ2"])
  
  return <div>
      <TagsInput tags={tags} onChangeTags={(newTags) => { setTags(newTags)} } />  
    </div>
}

ReactDOM.render(<App />,
document.getElementById('root'));

最後に

いかがだったでしょうか。
参考になったよっという方はぜひいいねよろしくお願いします!!

Discussion