😸
TailwindCSSとReactでタグ付けするInputを実装する
React上でフォームのInputに入力したものをタグとして表示させるUIコンポートを実装する必要があったのでそのメモになります。
簡易実装はこちらのCodePenを参考にしてください。
環境
- 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
の場合は処理をスキップしています。
入力文字が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