In this tutorial, I'll cover how to build a wysiwyg text editor in React js. Follows YouTube tutorial(CodeAT21) and GitHub repository full source code Available.
Setup
Create your react project. In your terminal/command line enter:
npx create-react-app text-editor
cd text-editor
npm install axios
npm install react-quill
npm start
Next, Project structure your folder as follows.
src
├── components
| ├── Add.js
| ├── Edit.js
| ├── Editpost.js
| ├── EditorToolbar.js
| ├── TextEditor.css
| └── Home.js
├── App.css
├── App.js
├── index.css
└── index.js
Next, open up the App.js file located in the src folder. You should see this:
App.jsimport React from 'react'; import {BrowserRouter as Router,Switch,Route} from "react-router-dom"; import './App.css'; import Home from './components/Home'; import Add from './components/Add'; import Edit from './components/Edit'; function App() { return ( <div> <Router basename="/"> <Switch> <Route exact path="/" component={Home}/> <Route path="/Add" component={Add}/> <Route path="/Edit/:postID" component={Edit}/> </Switch> </Router> </div> ); } export default App;
components/Home.jsimport React,{useState,useEffect} from 'react'; import {Link } from "react-router-dom"; import axios from 'axios'; function App() { useEffect(() => { viewPost(); }, []); const [ispost, setpost] = useState([]); const viewPost = async() =>{ try { await axios.get(`http://localhost:8080/allPost`,) .then(res => { if(res.data.success === true){ setpost(res.data.listall); } }) } catch (error) { throw error;} } return ( <div className="App"> <div className="container"> <div className="row"> <h1 className="App__tittle"> React <span> Quill </span> powerful rich text editor </h1> <Link to="/Add" className="btn btn__theme btn__add"> Create Now </Link> {ispost.map((item,index) => ( <div className="post__list" key={index}> <h2>{item.title}</h2> <div className="post__description" dangerouslySetInnerHTML={{ __html: item.description}} /> <div className="post__description" dangerouslySetInnerHTML={{ __html: item.information}} /> <Link to={`/Edit/${item.id}`} className="btn btn__theme"> Edit </Link> </div> ))} </div> </div> </div> ); } export default App;
components/Add.jsimport React, { useCallback, useEffect, useState } from "react"; import ReactQuill from "react-quill"; import EditorToolbar, { modules, formats } from "./EditorToolbar"; import "react-quill/dist/quill.snow.css"; import "./TextEditor.css"; import { useHistory } from "react-router-dom"; import axios from 'axios'; function Add() { let history = useHistory(); const [userInfo, setuserInfo] = useState({ title: '', description: '', information: '', }); const onChangeValue = (e) => { setuserInfo({ ...userInfo, [e.target.name]:e.target.value }); } const ondescription = (value) => { setuserInfo({ ...userInfo, description:value }); } const oninformation = (value) => { setuserInfo({ ...userInfo, information:value }); } const [isError, setError] = useState(null); const addDetails = async (event) => { try { event.preventDefault(); event.persist(); if(userInfo.description.length < 50){ setError('Required, Add description minimum length 50 characters'); return; } axios.post(`http://localhost:8080/addArticle`, { title: userInfo.title, description: userInfo.description, information: userInfo.information, }) .then(res => { if(res.data.success === true){ history.push('/'); } }) } catch (error) { throw error;} } return ( <> <div className="App"> <div className="container"> <div className="row"> <form onSubmit={addDetails} className="update__forms"> <h3 className="myaccount-content"> Add </h3> <div className="form-row"> <div className="form-group col-md-12"> <label className="font-weight-bold"> Title <span className="required"> * </span> </label> <input type="text" name="title" value={userInfo.title} onChange={onChangeValue} className="form-control" placeholder="Title" required /> </div> <div className="clearfix"></div> <div className="form-group col-md-12 editor"> <label className="font-weight-bold"> Description <span className="required"> * </span> </label> <EditorToolbar toolbarId={'t1'}/> <ReactQuill theme="snow" value={userInfo.description} onChange={ondescription} placeholder={"Write something awesome..."} modules={modules('t1')} formats={formats} /> </div> <br /> <div className="form-group col-md-12 editor"> <label className="font-weight-bold"> Additional Information </label> <EditorToolbar toolbarId={'t2'}/> <ReactQuill theme="snow" value={userInfo.information} onChange={oninformation} placeholder={"Write something awesome..."} modules={modules('t2')} formats={formats} /> </div> <br /> {isError !== null && <div className="errors"> {isError} </div>} <div className="form-group col-sm-12 text-right"> <button type="submit" className="btn btn__theme"> Submit </button> </div> </div> </form> </div> </div> </div> </> ) } export default Add
components/Edit.jsimport React,{useState,useEffect} from 'react'; import axios from 'axios'; import Editpost from './Editpost'; const Edit = (props) => { useEffect(() => { viewPostId(props.match.params.postID); }, []); const [ispostId, setpostId] = useState([]); const viewPostId = async(ids) =>{ try { await axios.post(`http://localhost:8080/getPostId`,{ ids: props.match.params.postID }) .then(res => { if(res.data.success === true){ setpostId(res.data.listId); } }) } catch (error) { throw error;} } return ( <> {ispostId.length > 0 ? <> <Editpost postList={ispostId} editPostID={props.match.params.postID} /> </> : null } </> ) } export default Edit
components/Editpost.jsimport React,{useState} from 'react'; import ReactQuill from "react-quill"; import EditorToolbar, { modules, formats } from "./EditorToolbar"; import "react-quill/dist/quill.snow.css"; import "./TextEditor.css"; import { useHistory } from "react-router-dom"; import axios from 'axios'; function Editpost(props) { let history = useHistory(); const [userInfo, setuserInfo] = useState({ title: props.postList[0].title, description: props.postList[0].description, information: props.postList[0].information, }); const onChangeValue = (e) => { setuserInfo({ ...userInfo, [e.target.name]:e.target.value }); } const ondescription = (value) => { setuserInfo({ ...userInfo, description:value }); } const oninformation = (value) => { setuserInfo({ ...userInfo, information:value }); } const [isError, setError] = useState(null); const PoemAddbooks = async (event) => { try { event.preventDefault(); event.persist(); if(userInfo.description.length < 50){ setError('Required, Add description minimum length 50 characters'); return; } axios.post(`http://localhost:8080/editArticle`, { title: userInfo.title, description: userInfo.description, information: userInfo.information, ids:props.editPostID }) .then(res => { // then print response status if(res.data.success === true){ history.push('/'); } }) } catch (error) { throw error;} } return ( <div className="App"> <div className="container"> <div className="row"> <form onSubmit={PoemAddbooks} className="update__forms"> <h3 className="myaccount-content"> Edit </h3> <div className="form-row"> <div className="form-group col-md-12"> <label className="font-weight-bold"> Title <span className="required"> * </span> </label> <input type="text" name="title" value={userInfo.title} onChange={onChangeValue} className="form-control" placeholder="Title" required /> </div> <div className="form-group col-md-12 editor"> <label className="font-weight-bold"> Description <span className="required"> * </span> </label> <EditorToolbar toolbarId={'t1'} /> <ReactQuill theme="snow" value={userInfo.description} onChange={ondescription} placeholder={"Write something awesome..."} modules={modules('t1')} formats={formats} /> </div> <br /> <div className="form-group col-md-12 editor"> <label className="font-weight-bold"> Additional Information </label> <EditorToolbar toolbarId={'t2'} /> <ReactQuill theme="snow" value={userInfo.information} onChange={oninformation} placeholder={"Write something awesome..."} modules={modules('t2')} formats={formats} /> </div> <br /> {isError !== null && <div className="errors"> {isError} </div>} <div className="form-group col-sm-12 text-right"> <button type="submit" className="btn btn__theme"> Submit </button> </div> </div> </form> </div> </div> </div> ) } export default Editpost
components/EditorToolbar.jsimport React from "react"; import { Quill } from "react-quill"; // Custom Undo button icon component for Quill editor. You can import it directly // from 'quill/assets/icons/undo.svg' but I found that a number of loaders do not // handle them correctly const CustomUndo = () => ( <svg viewBox="0 0 18 18"> <polygon className="ql-fill ql-stroke" points="6 10 4 12 2 10 6 10" /> <path className="ql-stroke" d="M8.09,13.91A4.6,4.6,0,0,0,9,14,5,5,0,1,0,4,9" /> </svg> ); // Redo button icon component for Quill editor const CustomRedo = () => ( <svg viewBox="0 0 18 18"> <polygon className="ql-fill ql-stroke" points="12 10 14 12 16 10 12 10" /> <path className="ql-stroke" d="M9.91,13.91A4.6,4.6,0,0,1,9,14a5,5,0,1,1,5-5" /> </svg> ); // Undo and redo functions for Custom Toolbar function undoChange() { this.quill.history.undo(); } function redoChange() { this.quill.history.redo(); } // Add sizes to whitelist and register them const Size = Quill.import("formats/size"); Size.whitelist = ["extra-small", "small", "medium", "large"]; Quill.register(Size, true); // Add fonts to whitelist and register them const Font = Quill.import("formats/font"); Font.whitelist = [ "arial", "comic-sans", "courier-new", "georgia", "helvetica", "Inter", "lucida" ]; Quill.register(Font, true); // Modules object for setting up the Quill editor export const modules =(props)=>({ toolbar: { container: "#" + props, handlers: { undo: undoChange, redo: redoChange } }, history: { delay: 500, maxStack: 100, userOnly: true } }); // Formats objects for setting up the Quill editor export const formats = [ "header", "font", "size", "bold", "italic", "underline", "align", "strike", "script", "blockquote", "background", "list", "bullet", "indent", "link", "image", "video", "color", "code-block" ]; // Quill Toolbar component export const QuillToolbar = (props) => { return (<> {props.toolbarId !== undefined && <div id={props.toolbarId}> <span className="ql-formats"> <button className="ql-bold" /> <button className="ql-italic" /> <button className="ql-underline" /> <button className="ql-strike" /> </span> <span className="ql-formats"> <select className="ql-font"> <option value="arial" > Arial </option> <option value="comic-sans">Comic Sans</option> <option value="courier-new">Courier New</option> <option value="georgia">Georgia</option> <option value="helvetica">Helvetica</option> <option value="Inter" selected>Inter</option> <option value="lucida">Lucida</option> </select> <select className="ql-size"> <option value="extra-small">Extra Small</option> <option value="small">Small</option> <option value="medium" selected>Medium</option> <option value="large">Large</option> </select> <select className="ql-header"> <option value="1">Heading 1</option> <option value="2">Heading 2</option> <option value="3">Heading 3</option> <option value="4">Heading 4</option> <option value="5">Heading 5</option> <option value="6">Heading 6</option> <option value="" selected>Normal</option> </select> </span> <span className="ql-formats"> <button className="ql-list" value="ordered" /> <button className="ql-list" value="bullet" /> <button className="ql-indent" value="-1" /> <button className="ql-indent" value="+1" /> </span> <span className="ql-formats"> <button className="ql-script" value="super" /> <button className="ql-script" value="sub" /> <button className="ql-blockquote" /> <button className="ql-direction" /> </span> <span className="ql-formats"> <select className="ql-align" /> <select className="ql-color" /> <select className="ql-background" /> </span> <span className="ql-formats"> <button className="ql-link" /> <button className="ql-image" /> <button className="ql-video" /> </span> <span className="ql-formats"> <button className="ql-formula" /> <button className="ql-code-block" /> <button className="ql-clean" /> </span> <span className="ql-formats"> <button className="ql-undo"> <CustomUndo /> </button> <button className="ql-redo"> <CustomRedo /> </button> </span> </div> } </>) } export default QuillToolbar;
MySQL database
In the following example, we will start how to React wysiwyg text editor into the MySQL database using Node js.
- Database Name – codeat21
- Table Name – posts
codeat21CREATE TABLE `posts` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, `description` longtext COLLATE utf8mb4_unicode_ci NOT NULL, `information` longtext COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Next, Node js project structure your folder as follows.
├── config
| ├── database.js
├── index.js
├── package.js
└── package-lock.js
database.jsconst util = require('util'); const mysql = require('mysql2'); const pool = mysql.createPool({ connectionLimit: 10, host : 'localhost', user : 'root', password : '', database : 'codeat21' }); // Ping database to check for common exception errors. pool.getConnection((err, connection) => { if (err) { if (err.code === 'PROTOCOL_CONNECTION_LOST') { console.error('Database connection was closed.'); } if (err.code === 'ER_CON_COUNT_ERROR') { console.error('Database has too many connections.'); } if (err.code === 'ECONNREFUSED') { console.error('Database connection was refused.'); } } if (connection) connection.release(); return; }); // Promisify for Node.js async/await. pool.query = util.promisify(pool.query); module.exports = pool;
index.jsconst express = require('express'); const app = express(); const path = require('path'); const cors = require('cors'); const bodyParser = require('body-parser'); const port = process.env.PORT || 8080; // Databse Connection const db_connection = require('./config/database').promise(); app.use(cors()); app.use(bodyParser.json() ); app.use(bodyParser.urlencoded({extended:true})); app.get('/allPost', async (req, res) => { try { const [rows] = await db_connection.execute("SELECT * FROM posts "); return res.json({ success: true, listall:rows, }); } catch (err) {console.log(err)} }); app.post('/addArticle', async (req, res) => { try { const [rows] = await db_connection.execute("INSERT INTO `posts` (`title`,`description`,`information`) VALUES(?, ?, ?)",[req.body.title,req.body.description,req.body.information]); if (rows.affectedRows === 1) { return res.json({ success: true}) } } catch (err) {console.log(err)} }); app.post('/getPostId', async (req, res) => { try { const [rows] = await db_connection.execute("SELECT * FROM posts where id = ? ",[req.body.ids]); if(rows.length > 0 ){ return res.json({ success: true, listId:rows,}) } } catch (err) {console.log(err)} }); app.post('/editArticle', async (req, res) => { try { const [update] = await db_connection.execute("UPDATE `posts` SET `title`=?, `description`=?,`information`=? WHERE id = ?",[req.body.title,req.body.description,req.body.information,req.body.ids]); if (update.affectedRows === 1) { return res.json({ success: true, }) } } catch (err) {console.log(err)} }); app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))