Tiptapでテキストエディタ実装できます

# はじめに

@Sicut_study さんの【Reactアプリ100本ノック】09 Memo をクリアするために、Tiptapを使用したので簡単に使い方をまとめます。

https://qiita.com/Sicut_study/items/a27e5789a1b23438d77c

# 成果物

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582046/7afa0257-123a-37fc-9883-dfa598070c27.png)


# Tiptapとは

リッチなテキストエディタをつくることができるものです。

多くの拡張機能が用意されているのが特徴です。

標準でエディタにスタイリングされていないため、UIを自由に実装できます。(このことをヘッドレスっていうのかな？)

https://tiptap.dev/docs/editor/introduction

# セットアップ

Tiptapは以下の環境での使用を提供しています。

- Vanilla JavaScript
- React
- Next.js
- Vue 3
- Vue 2
- Nuxt.js
- Svelte
- Alpine.js
- PHP
- CDN

https://tiptap.dev/docs/editor/installation

私は今回`Next.js`でセットアップをしていきます。

### プロジェクト作成

```shell:ターミナル
npx create-next-app my-tiptap-project
```

### 作成したプロジェクトに移動

```shell:ターミナル
cd my-tiptap-project
```

### インストール

`tiptap`のインストールのついでに、便利な`スターターキット`もインストールします。

```shell:ターミナル
npm install @tiptap/react @tiptap/pm @tiptap/starter-kit
```

### Tiptap.tsxを作成

`/components/Tiptap.tsx`を作成して以下を貼り付けましょう。

```tsx:components/Tiptap.tsx
'use client'

import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

const Tiptap = () => {
  const editor = useEditor({
    extensions: [
      StarterKit,
    ],
    content: '<p>Hello World! 🌎️</p>',
  })

    if (!editor) {
        return null
    }

  return (
    <EditorContent editor={editor} />
  )
}

export default Tiptap
```

### page.tsxを作成

先ほど作った`Tiptap.tsx`を表示するために`app/page.tsx`でインポートしましょう。

```jsx:app/page.tsx
import Tiptap from '../components/Tiptap'

export default function Home() {
    return (
         <Tiptap />
    )
}
```

### 確認

正常に動いているかローカルサーバを立ち上げて確認しましょう。

```shell:ターミナル
npm run dev
```

下の画像のようになっていれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582046/4cf468b2-7fbb-08da-9269-18c7522cd23f.png)

# ちょっといじる

このままではしょぼいのでちょっといじりましょう。

### 拡張機能

https://tiptap.dev/docs/editor/extensions

沢山の拡張機能があるのでいくつかご紹介します。

### ToolMenu.tsxを作成

ツールを表示するコンポーネント`/components/ToolMenu.tsx`を作成します。

```jsx:/componets/ToolMenu.tsx
import { Editor } from '@tiptap/react'
import {
    MdFormatBold,
    MdFormatStrikethrough,
    MdRedo,
    MdUndo,
} from 'react-icons/md'

const ToolMenu = ({ editor }: { editor: Editor }) => {
    if (!editor) {
        return null
    }

    return (
        <div className="flex flex-wrap gap-2 border-b border-gray-600 p-4 text-2xl">
            <button
                type="button"
                onClick={() => editor.chain().focus().toggleBold().run()}
                className={!editor.isActive('bold') ? 'opacity-20' : ''}
            >
                <MdFormatBold />
            </button>
            <button
                type="button"
                onClick={() => editor.chain().focus().toggleStrike().run()}
                className={!editor.isActive('strike') ? 'opacity-20' : ''}
            >
                <MdFormatStrikethrough />
            </button>
            <button
                onClick={() => editor.chain().focus().undo().run()}
                type="button"
            >
                <MdUndo />
            </button>
            <button
                onClick={() => editor.chain().focus().redo().run()}
                type="button"
            >
                <MdRedo />
            </button>
        </div>
    )
}

export default ToolMenu

```

`button`の`onCilck`属性に色々メソッドを割り当てることで様々な機能を使用できます。

今回は、コードの上から順に、

- 太字
- 取り消し線
- 一つ戻る
- 一つ進む

を実装しています。

各ボタンのアイコンには[React Icons](https://react-icons.github.io/react-icons/)を使用しています。

### 確認

再度ローカルサーバを立ち上げて正常に実装できているか確認しましょう。

下の画像のようにツールメニューに４つのアイコンが表示されていれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582046/49e9a59e-0ddc-5fdc-9b88-0c2a0352ab96.png)

# stateで内容を管理する

エディタの内容を`state`で管理したいことあると思います。

その方法を書いときます。

### page.tsxを編集

```diff_jsx:app/page.tsx
+ 'use client'

import Tiptap from '@/features/Tiptap/components/Editor'
+ import { useState } from 'react'

export default function Home() {
+    const [sentence, setSentence] = useState<string>('初期値')
    return (
        <>
+            <p>state = {sentence}</p>
+            <Tiptap sentence={sentence} setSentence={setSentence} />
        </>
    )
}
```

`useState`を使うので、先頭に`use client`を付けて`クライアントコンポーネント`にします。

`pタグ`で`useState`の内容を逐次表示します。

`TipTap`コンポーネントに`state`と`更新用関数`を渡します。

### Tiptap.tsxを編集

```diff_jsx:components/Tiptap.tsx
'use client'

import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import ToolMenu from './ToolMenu'

+ type Props = {
+     sentence: string
+     setSentence: (sentence: string) => void
+ }

+ const Tiptap = ({ sentence, setSentence }: Props) => {
    const editor = useEditor({
        extensions: [StarterKit],
+       content: sentence,
        editorProps: {
            attributes: {
                class: 'prose prose-base m-5 focus:outline-none',
            },
        },
    })

+   if (editor) {
+       editor.on('update', () => {
+           setSentence(editor.getText())
+       })
+   }

    if (!editor) {
        return null
    }

    return (
        <div className="w-2/3 mt-10 mx-auto border-gray-500 border-2">
            <ToolMenu editor={editor} />
            <div className="p-3 overflow-y-scroll h-[70vh] overflow-hidden mt-3">
                <EditorContent editor={editor} />
            </div>
        </div>
    )
}

export default Tiptap
```

`content`にはエディタの初期値を格納できるので、`useState`で管理するため`sentence`を入れています。

`Tiptap`が提供している`イベント`の中に、`update`というものがあります。

これを使うことでエディタが更新されたら、何かしらの関数を発火させることができます。

`editor.getText()`で現在のエディタの内容を文字列で取得し、`setSentence`を使って、`sentence`を更新しています。

### 確認

再度ローカルサーバを立ち上げて正常に実装できているか確認しましょう。

下の画像のようにエディタの内容と上の`state`が連動していれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582046/28306c27-bee0-951b-0aff-c96f04247ff6.png)

# おわりに

便利ものツールはどんどん利用しましょう。

# 参考にさせていただいた記事

この方のコードをかなり参考にさせていただきました。

https://zenn.dev/kodaishoituki/articles/f1763ded8b5714