【react-quill】簡単にリッチテキストエディタを実装する
react-quill とは何か?
Quillとは、オープンソースのWYSIWYGエディタをWeb上で実現するためのjsプラグインです。
QuillをReact上で使えるようにしたパッケージがreact-quillです。
react-quillの使い方
react-quillのオプションは以下の通りです。
id
: 指定するとDOM要素に当該IDが付与されますclassName
: 指定するとDOM要素に当該ClassNameが付与されますvalue
: エディタに表示する値。「HTMLを含む文字列、Quill Delta インスタンス、または Deltaを表すプレーン オブジェクト」を指定できるみたい。defaultValue
: エディタに表示する初期値readOnly
: trueを設定すると編集できなくなります。placeholder
: エディタが空の時に表示する文字列。動的に変更したい時はrefsとdata-attributesを使え、とのこと。modules
: エディターのツールバーなどに適用するモジュールformats
: 編集中に有効にするフォーマットを配列で指定する。デフォルトでは全て有効になっています。style
: CSSに適用するスタイル。キャメルケースで指定すること。theme
: 適用するテーマ名。デフォルトは「snow」tabIndex
: エディタがフォーカスする順番bounds
: ポップアップの位置を制限するために Quill によって使用されるセレクターまたは DOM 要素。デフォルトは document.bodychildren
: デフォルトの代わりに編集領域として使用される要素。textareaは指定できないonChange
: 編集後に新しい内容で実行されるコールバックonChangeSelection
: 新しい選択値で実行されるコールバックonFocus
: フォーカスされた時に実行するコールバックonBlur
: フォーカスを失った時に実行するコールバックonKeyPress
: キーを押して離した時に実行するコールバックonKeyDown
: キーを押して離す前に実行するコールバックonKeyUp
: キーを話す時に実行するコールバックpreserveWhitespace
: true の場合、デフォルトの div タグの代わりに pre タグがエディター領域に使用されます
react-quill でブログ管理システムを作ってみる
さっそくブログ管理システムを作ってみたいと思います。
まず、プロジェクトの雛形を作成します。vite を使用してJavaScript,Reactを選択し、プロジェクト名はreact-quill-sample
とします。
npm create vite@latest
Need to install the following packages:
create-vite@4.2.0
Ok to proceed? (y) y
✔ Project name: … react-quill-sample
✔ Select a framework: › React
✔ Select a variant: › JavaScript
Scaffolding project in /react-quill-sample...
Done. Now run:
ディレクトリを移動して、インストール、ローカルホストを立ち上げます。
cd react-quill-sample
npm install
npm run dev
必要なパッケージをインストールします。CSSはBootstrap
を、react-quill
で作成したHTMLを直接埋め込むためにhtml-react-parser
を使用します。
npm install --save react-quill html-react-parser bootstrap reactstrap
次にBootstrapとreact-quill
のCSSファイルのセットアップを行います。
不要なCSSファイルの読み込みを削除するのと以下の2行でsrc/main.jsx
でCSSファイルの読み込みを行います。
import "react-quill/dist/quill.snow.css";
import "bootstrap/dist/css/bootstrap.css";
src/main.jsx
のファイル全体は以下のようになります。これでセットアップは完了です。
./src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "react-quill/dist/quill.snow.css";
import "bootstrap/dist/css/bootstrap.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
次にContextAPIでブログを管理するためにContextProviderを定義します。ブログの内容はid
, title
, content
, createdAt
となります。
./src/BlogInfoProvider.jsx
import { createContext, useState } from "react";
export const BlogInfoContext = createContext();
export const BlogInfoProvider = ({ children }) => {
const [blogs, setBlogs] = useState([]);
const [id, setId] = useState();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [registerModal, setRegisterModal] = useState(false);
const toggleRegisterModal = () => setRegisterModal(!registerModal);
const [editModal, setEditModal] = useState(false);
const toggleEditModal = () => setEditModal(!editModal);
const addBlog = () => {
const lastBlog = blogs[blogs.length - 1];
const id = lastBlog ? lastBlog.id + 1 : 1;
const newBlog = {
id,
content,
title,
createdAt: new Date(),
};
setBlogs([...blogs, newBlog]);
toggleRegisterModal();
setId();
setTitle("");
setContent("");
};
const updateBlog = () => {
const newBlog = {
id,
content,
title,
};
const newBlogs = blogs.map((blog) => {
if (blog.id === id) {
return { ...blog, ...newBlog };
} else {
return blog;
}
});
setBlogs(newBlogs);
toggleEditModal();
setTitle("");
setContent("");
};
const deleteBlog = (i) => {
if (confirm("本当に削除しますか?")) {
const newBlogs = [...blogs];
newBlogs.splice(i, 1);
setBlogs(newBlogs);
}
};
return (
<BlogInfoContext.Provider
value={{
blogs,
setBlogs,
title,
setTitle,
content,
setContent,
registerModal,
setRegisterModal,
toggleRegisterModal,
editModal,
setEditModal,
addBlog,
deleteBlog,
updateBlog,
setId,
}}
>
{children}
</BlogInfoContext.Provider>
);
};
次にmain.jsxで先ほど作成したBlogInfoProvider
でApp
コンポーネントを囲います。
./src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "react-quill/dist/quill.snow.css";
import "bootstrap/dist/css/bootstrap.css";
import { BlogInfoProvider } from "./BlogInfoProvider";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BlogInfoProvider>
<App />
</BlogInfoProvider>
</React.StrictMode>
);
次にブログの一覧を表示するためのコンポーネントを作成します。
一覧画面にはID、タイトル、内容、作成日時を表示します。対象の記事を削除、更新するためのボタンも設置します。
./src/BlogList.jsx
import React, { useContext } from "react";
import { Button, Table } from "reactstrap";
import parse from "html-react-parser";
import Modal from "./Modal";
import { BlogInfoContext } from "./BlogInfoProvider";
function BlogList() {
const {
setTitle,
setContent,
blogs,
deleteBlog,
editModal,
toggleEditModal,
updateBlog,
setEditModal,
setId,
} = useContext(BlogInfoContext);
const handleEditModal = (i) => {
setTitle(blogs[i].title);
setContent(blogs[i].content);
setId(i + 1)
setEditModal(!editModal);
};
return (
<Table>
<thead>
<tr>
<th>#</th>
<th>タイトル</th>
<th>内容</th>
<th>作成日時</th>
<th>更新</th>
<th>削除</th>
</tr>
</thead>
<tbody>
{blogs.map((blog, i) => (
<tr key={i}>
<th scope="row">{blog.id}</th>
<td>{blog.title}</td>
<td>{parse(blog.content)}</td>
<td>{blog.createdAt.toLocaleString()}</td>
<td>
<Button color="primary" onClick={() => handleEditModal(i)}>
更新
</Button>
<Modal
modal={editModal}
toggle={toggleEditModal}
onClick={() => updateBlog()}
/>
</td>
<td>
<Button color="danger" onClick={() => deleteBlog(i)}>
削除
</Button>
</td>
</tr>
))}
</tbody>
</Table>
);
}
export default BlogList;
次は記事を作成するためのフォームのモーダル実装です。ここでreact-quillを使用します。使用箇所は以下の部分です。
<ReactQuill
id="content"
theme="snow"
value={content}
onChange={setContent}
/>
Modal
コンポーネントは記事の新規作成時と、既存記事の更新時に呼ばれるコンポーネントです。
./src/Modal.jsx
import React, { useContext } from "react";
import {
Button,
Modal as BootstrapModal,
ModalHeader,
ModalBody,
ModalFooter,
Label,
Input,
Form,
FormGroup,
} from "reactstrap";
import ReactQuill from "react-quill";
import { BlogInfoContext } from "./BlogInfoProvider";
function Modal({ modal, toggle, onClick }) {
const { title, setTitle, content, setContent } = useContext(BlogInfoContext);
return (
<BootstrapModal isOpen={modal} toggle={toggle}>
<ModalHeader toggle={toggle}>
ブログ記事を追加する
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label for="title">タイトル</Label>
<Input
id="title"
name="title"
placeholder="タイトルを入力"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</FormGroup>
<FormGroup>
<Label for="content">内容</Label>
<ReactQuill
id="content"
theme="snow"
value={content}
onChange={setContent}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={toggle}>
キャンセル
</Button>
<Button color="primary" onClick={onClick}>
保存
</Button>
</ModalFooter>
</BootstrapModal>
);
}
export default Modal;
最後にApp.jsxを実装します。
./src/App.jsx
import { Button } from "reactstrap";
import Modal from "./Modal";
import BlogList from "./BlogList";
import { BlogInfoContext } from "./BlogInfoProvider";
import { useContext } from "react";
function App() {
const { toggleRegisterModal, addBlog, registerModal } =
useContext(BlogInfoContext);
return (
<>
<h2>ブログ管理システム</h2>
<Button color="primary" onClick={toggleRegisterModal}>
ブログ記事を書く
</Button>
<Modal
modal={registerModal}
toggle={toggleRegisterModal}
onClick={addBlog}
/>
<BlogList />
</>
);
}
export default App;