Quilljs is a free, open-source library that lets developers easily add rich text editing capabilities to their web applications. It provides a familiar WYSIWYG (What You See Is What You Get) editing experience, similar to popular word processors, allowing users to format text, add images, and create interactive content. It's also known for being customizable, so developers can tailor it to their specific needs.
While React Quill was once a popular option for adding rich text editing to React applications, there are a couple reasons why it might not be the most relevant choice these days:
- Out of Date: React Quill hasn't had major updates in over two years. This raises concerns about compatibility with newer React versions and security risks.
- Quill v2 Issues: There's a new version of Quill (v2), but React Quill doesn't seem to work with it smoothly, potentially causing problems.
- Outdated Techniques: React Quill might use older methods for handling the web page structure (DOM manipulation) that aren't ideal for modern React development.
There's a new React library called React Quilljs that's actively maintained and integrates well with React projects.
I'll show you how to use React Quilljs with React Hook Form in a TypeScript project. While it might not be the ultimate solution, it works!
Install dependencies
npm install react-quilljs quill
npm install -D @types/quill
npm install react-hook-form
For other configuration options, check out the react-quilljs documentation.
Usage
First, create a file named App.tsx
in the root directory. This file will be used to render the editor. Be caution when using this file like this, because it will not work without the Layout.tsx
component (see the project on GitHub)
import React, { useState } from "react"; | |
import { Controller, useForm, SubmitHandler } from "react-hook-form"; | |
import { Layout } from "./components/Layout"; | |
import { Editor } from "./components/Editor"; | |
interface IFormInput { | |
title: string; | |
content: string; | |
} | |
export default function App() { | |
const [content, setContent] = useState(""); | |
const [output, setOutput] = useState(""); | |
const { | |
control, | |
register, | |
handleSubmit, | |
formState: { errors }, | |
} = useForm<IFormInput>(); | |
const onSubmit: SubmitHandler<IFormInput> = (data) => { | |
const newPost = { title: data.title, content: content }; | |
setOutput(JSON.stringify(newPost, undefined, 2)); | |
}; | |
return ( | |
<Layout> | |
<form onSubmit={handleSubmit(onSubmit)}> | |
<div className="space-y-12"> | |
<div className="pb-12"> | |
<h2 className="text-base font-semibold leading-7 text-gray-900"> | |
New post | |
</h2> | |
<p className="mt-1 text-sm leading-6 text-gray-600"> | |
Add new post to your blog. | |
</p> | |
<div className="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> | |
<div className="sm:col-span-4"> | |
<label | |
htmlFor="title" | |
className="block text-sm font-medium leading-6 text-gray-900" | |
> | |
Title * | |
</label> | |
<div className="mt-2"> | |
<div className="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600 sm:max-w-md"> | |
<input | |
type="text" | |
className="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6" | |
placeholder="Enter post title" | |
{...register("title", { required: true })} | |
/> | |
</div> | |
{errors.title && ( | |
<p className="mt-2 text-red-500 text-sm"> | |
This field is required | |
</p> | |
)} | |
</div> | |
</div> | |
<div className="col-span-full"> | |
<label | |
htmlFor="about" | |
className="block text-sm font-medium leading-6 text-gray-900" | |
> | |
Content | |
</label> | |
<div className="mt-2"> | |
<Controller | |
name="content" | |
control={control} | |
render={({ field: { value } }) => ( | |
<Editor | |
onChange={setContent} | |
value={value} | |
placeholder="Write something..." | |
/> | |
)} | |
/> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div className="mt-3 col-span-full"> | |
<textarea | |
readOnly | |
rows={5} | |
className="block w-full rounded-md border-0 py-1.5 pl-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" | |
value={output} | |
placeholder="Click on the Save button to view results..." | |
/> | |
</div> | |
<div className="mt-6 flex items-center justify-end gap-x-6"> | |
<button | |
type="button" | |
className="text-sm font-semibold leading-6 text-gray-900" | |
> | |
Cancel | |
</button> | |
<button | |
type="submit" | |
className="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" | |
> | |
Save | |
</button> | |
</div> | |
</form> | |
</Layout> | |
); | |
} |
Then, create a file named Editor.tsx
in the components
directory. This file will be used to render the editor.
import React from "react"; | |
import { useQuill } from "react-quilljs"; | |
import "quill/dist/quill.snow.css"; | |
import "../styles/quill.editor.css"; | |
interface EditorProps { | |
value: string; | |
placeholder: string; | |
onChange: (value: string) => void; | |
} | |
export const Editor = ({ value, placeholder, onChange }: EditorProps) => { | |
const theme = "snow"; | |
const modules = { | |
toolbar: [ | |
["bold", "italic", "underline", "strike"], | |
[{ align: [] }], | |
[{ list: "ordered" }, { list: "bullet" }], | |
[{ indent: "-1" }, { indent: "+1" }], | |
[{ header: [1, 2, 3, 4, 5, 6, false] }], | |
["link"], | |
[{ color: [] }], | |
], | |
}; | |
const formats = [ | |
"bold", | |
"italic", | |
"underline", | |
"strike", | |
"align", | |
"list", | |
"indent", | |
"header", | |
"link", | |
"color", | |
]; | |
const { quill, quillRef } = useQuill({ | |
theme, | |
modules, | |
formats, | |
placeholder, | |
}); | |
React.useEffect(() => { | |
if (quill) { | |
if (value) { | |
quill.clipboard.dangerouslyPasteHTML(value); | |
} | |
quill.on("text-change", () => { | |
onChange(quill.root.innerHTML); | |
}); | |
} | |
}, [quill, value, onChange]); | |
return ( | |
<div style={{ height: 200 }}> | |
<div ref={quillRef} /> | |
</div> | |
); | |
}; |
Conclusion
You can view results on CodeSandbox
The project is hosted on GitHub
Thank you
Life's too short for boring endings.
Top comments (0)