Demo trang editor đơn giản sử dụng Markdown và React.

Demo trang editor đơn giản sử dụng Markdown và React.

Những website thể hiện thông tin dưới dạng bài viết được cập nhật liên tục như blog, báo, tuyển dụng... luôn có một trang (mình tạm gọi) là Tạo mới. Đặt ra trường hợp chúng ta chỉ để một ô text để người dùng nhập chữ thuần vào thì thật là nhàm chán 😅. Vì vậy trong bài viết này, mình sẽ hướng dẫn tạo một Markdown editor cho phép người dùng tự do format nội dung theo ý muốn.

Tạo project Reactjs mới

Đầu tiên tạo mới một project có tên demo-markdown-editor

yarn create react-app demo-markdown-editor
cd demo-markdown-editor
yarn start

Để trang web chỉnh chu hơn, bạn có thể cài thêm UI library (như Ant Design, Material UI,...) hoặc CSS framework (như Tailwind CSS,...).

Set up Markdown

Cài đặt react-markdownremark-gfm

yarn add react-markdown
yarn add remark-gfm

react-markdown giúp chuyển đổi mardown text thành HTML một cách an toàn (sử dụng DOM ảo thay cho dangerouslySetInnerHTML), được hỗ trợ bởi remark-gfm. Để hiểu hơn, hãy thử chạy đoạn code sau trong App.js

import './App.css';
import ReactMarkdown from 'react-markdown'
import gfm from 'remark-gfm'

const content =
  `A paragraph with *emphasis* and **strong importance**.
> A block quote with ~strikethrough~ and a URL: https://reactjs.org.
* Lists
* [ ] todo
* [x] done
A table:
| a | b |
| - | - |
`

export default function App() {
  return (
    <div>
      <ReactMarkdown plugins={[gfm]} children={content} />
    </div>
  );
}

Kết quả trả về như sau: demoMD.PNG Vậy là hoàn thành cơ bản cách dùng Markdown rồi 😄, chúng ta tiếp tục bước tiếp theo thôi.

Demo trang editor đơn giản

Chúng ta sẽ xây dựng một UI đơn giản chứa 2 phân vùng: nơi để user viết content và nơi hiển thị xem trước bài viết, có thể thêm 1 phân vùng nữa gọi là Basic use Markdown. Sửa App.js lại thành

export default function App() {
  const [content, setContent] = useState("")
  const [result, setResult] = useState("")

  useEffect(() => {
    const timeout = setTimeout(() => {
      setResult(content)
    }, 800)
    return () => clearTimeout(timeout)
  }, [content])

  return (
    <div>
      <textarea
        type="text"
        value={content}
        onChange={e => setContent(e.target.value)}
      />
      <ReactMarkdown plugins={[gfm]} children={result} />
    </div>

  );
}

Mình thêm timeout quy định chỉ trả về kết quả sau khi user dừng nhập được 0.8s, khắc phục việc ngay lập tức Result hiển thị khi Content được change. Tiếp tục thêm một thanh công cụ để format tự động:

// yarn add react-icons
import { RiDoubleQuotesL, RiCodeSSlashFill, RiLinksFill, RiListCheck, RiListOrdered, RiImage2Line } from 'react-icons/ri'
...
<div>
  <ul>
    <li onClick={addHeading1}>H1</li>
    <li onClick={addHeading2}>H2</li>
    <li onClick={addHeading3}>H3</li>
    <li onClick={addHeading4}>H4</li>
    <li onClick={addHeading5}>H5</li>
    <li onClick={addHeading6}>H6</li>
    <li onClick={addBoldText} title="Bold"><b>B</b></li>
    <li onClick={addItalicText} title="Italic"><i>I</i></li>
    <li onClick={addQuote} title="Quote"><RiDoubleQuotesL /></li>
    <li onClick={addBlockCode} title="Block code"><RiCodeSSlashFill /></li>
    <li onClick={addLink} title="Link"><RiLinksFill /></li>
    <li onClick={addList} title="List"><RiListCheck /></li>
    <li onClick={addOrderedList} title="Ordered List"><RiListOrdered /></li>
    <li onClick={addImage} title="Image"><RiImage2Line /></li>
  </ul>
</div>
...

Các bạn có thể làm gọn toolbar bằng cách thay thế các Heading bằng một dropdown. Để hoàn thiện, hãy thêm các phương thức tương ứng với mỗi công cụ theo cú pháp của Markdown. Ở đây mình mặc định sẽ xuống hàng mỗi khi chèn (trừ bold và italic):

...
const addHeading1 = () => {
  const newContent = `${content}
# 
`
  setContent(newContent)
}

const addHeading2 = () => {
  const newContent = `${content}
## 
`
  setContent(newContent)
}

const addHeading3 = () => {
  const newContent = `${content}
### 
`
  setContent(newContent)
}

const addHeading4 = () => {
  const newContent = `${content}
#### 
`
  setContent(newContent)
}

const addHeading5 = () => {
  const newContent = `${content}
##### 
`
  setContent(newContent)
}

const addHeading6 = () => {
  const newContent = `${content}
###### 
`
  setContent(newContent)
}

const addBoldText = () => {
  const newContent = `${content} **Text**`
  setContent(newContent)
}

const addItalicText = () => {
  const newContent = `${content} *Text*`
  setContent(newContent)
}

const addQuote = () => {
  const newContent = `${content}
>     
`
  setContent(newContent)
}

const addBlockCode = () => {
  const blockCode = "```"
  const newContent = `${content}
${blockCode}
${blockCode}
`
  setContent(newContent)
}

const addLink = () => {
  const newContent = `${content}
[Text](Link)
`
  setContent(newContent)
}

const addList = () => {
  const newContent = `${content}
- 
`
  setContent(newContent)
}

const addOrderedList = () => {
  const newContent = `${content}
1. 
`
  setContent(newContent)
}

const addImage = () => {
  const newContent = `${content}
![Text](Link)
`
  setContent(newContent)
}
...

Chỉnh sửa CSS một chút cho dễ nhìn và test thử. Kết quả cuối cùng trông cũng rất gì và này nọ đó chứ 😂 md.PNG Để Result trả về xem trước ngon lành cành đào hơn (thêm màu chữ cho code, thêm các styles cho text,...), bạn nên cài thêm package css cho Markdown như github-markdown-css.

Tạm kết

Vậy là chúng ta đã tìm hiểu xong cách build một Markdown editor đơn giản rồi. Nhờ có Markdown chúng ta có thể dễ dàng thêm hình ảnh, link và format content giúp cho bài viết trở nên sống động hơn. Bạn có thể thử tự làm một blog cá nhân với nút Create New Post trả về trang tạo mới bài viết tương tự như demo trên. Chi tiết hơn có lẽ mình sẽ làm tiếp một chủ đề nữa về cách build một simple blog, bạn hãy đón đọc nhé.

Nếu có góp ý bạn hãy comment hoặc liên hệ với mình. Chúc bạn một ngày vui vẻ!