Custom image handler React-Quill
Tags: javascript, reactquill
Published: 2022-12-31 09:24:34


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p0oa7pbjs0lw1087rc6y.png)
Pada kali ini saya akan sharing cara mengubah handler module image bawaan dari react-quill. Karena handler module image react-quill mengahasilkan data base64, yang mana datanya akan panjang sendiri untuk 1 image dan membuat memory membengkak saat data html tagnya akan disimpan ke database. Dan disini saya akan mengubah alur upload image dengan mengirim imagenya ke upload service yang disediakan BE untuk disimpan ke dalam local file system dan mengahasilkan response url string.


Kita mulai dengan menginstall react-quill
```
npm i react-quill
npm i quill
```
Berikut sample react-quill menggunakan handlers defaultnya 

```
import ReactQuill from "react-quill";
import Quill from "quill";
import "react-quill/dist/quill.snow.css";

const RichContentEditor = () => {
    const modules = React.useMemo(
    () => ({
      toolbar: {
        container: [
          [{ font: [] }],
          [{ header: [2, 3, false] }],
          [{ align: [] }],
          ["bold", "italic", "underline"],
          [{ list: "ordered" }, { list: "bullet" }],
          ["link", "image", "video"],
          ["clean"],
        ],
      },
      history: {
        delay: 500,
        maxStack: 100,
        userOnly: true,
      },
    }),
    []
  );
  
return (
    <>
      <ReactQuill
        id={"richContent"}
        modules={modules}
        formats={[
          "header",
          "font",
          "bold",
          "italic",
          "underline",
          "list",
          "bullet",
          "align",
          "link",
          "image",
          "video",
        ]}
        placeholder={""}
        onChange={(e) => setContentValue(e)}
        value={contentValue}
        theme="snow"
      />
    </>
  );
}
```
dibawah ini contoh yang sudah menggunakan custom image handlersnya, untuk fungsi-fungsinya saya cantumkan dalam comment saja, saya hanya menjelaskan custom image handlers disini untuk membuka modal yang berisikan upload image dan form input property image yang dibutuhkan untuk dikirimkan ke upload service dan meng-embed url imagenya.
```
import React, { useRef, useState } from "react";
import ReactQuill from "react-quill";
import { Button, Form, Input, Modal } from "tunggal";
import { UploadPreview } from "pages/product/detail/detail.styles";
import Quill from "quill";
import "react-quill/dist/quill.snow.css";
import IPayloadAPI from "interfaces/iapi";
import upload from "libraries/api/upload";
import { callAPI } from "utils/fetchData";
import { errorNotification, formatBytes } from "utils/commonFunction";
import { ListStyled } from "pages/product/variant/variant.styles";
import { initialImage } from "helpers/constants";
import { default as UploadDrag } from "rc-upload";
import ImageBlot from "./image-blot";

const RichContentEditor = () => {
  let reactQuillRef = useRef(null); // ReactQuill component
  let quillRef = useRef(null); // Quill instance

  const [showModalImage, setShowModalImage] = useState(false);
  const [contentValue, setContentValue] = useState("");
  const [isFetchingUpload, setIsFetchingUpload] = useState(false);
  const [titleImage, setTitleImage] = useState("");
  const [altImage, setAltImage] = useState("");
  const [selectedImage, setSelectedImage] = useState(initialImage);
  
  // show / close modal
  const handleShowModal = () => {
    setShowModalImage(!showModalImage);
    handleReset();
  };

  const handleReset = () => {
    setAltImage("");
    setTitleImage("");
    setSelectedImage(initialImage);
  };

  // function onchange property image
  const onChangeInput = (key: string, e: string) => {
    const valueRegex = e.replace(/[^\w\s]/gi, "");
    if (key === "title") {
      setTitleImage(valueRegex);
    } else {
      setAltImage(valueRegex);
    }
  };

  // simpan fileImage ke dalam state
  const onUpload = async (e) => {
    const file = e;
    if (file) {
      setSelectedImage({
        image: file,
        size: file.size,
        fileName: file.name,
      });
    }
    e.target.value = null;
  };

  // function cek fokus index terakhir element input react-quill
  const handleSetQuillRef = () => {
    if (typeof reactQuillRef.current.getEditor !== "function") {
      return;
    }
    const temp = reactQuillRef.current.getEditor();
    quillRef.current = temp;
  };

  // function Request ke upload Service yang menghasilkan url image
  const uploadImage = async () => {
    if (selectedImage.image) {
      handleSetQuillRef();

      const range = quillRef.current.getSelection(true);
      const titles = titleImage ? titleImage : selectedImage.fileName;

      const fileData = new FormData();
      fileData.append("file", selectedImage.image);
      fileData.append("type", "richcontent");
      fileData.append("uploadType", "image");
      fileData.append("filename", titles);
      let params: IPayloadAPI = upload.uploadFile(fileData);
      setIsFetchingUpload(true);
      try {
        const response = await callAPI(params);
        const { data } = response.data;

        // proses mengembed image pada range index terakhir
        quillRef.current.insertEmbed(
          range.index,
          "image",
          {
            alt: altImage, // untuk memasukkan property ALT image
            title: titles, // untuk memasukkan property title image
            url: data.original, // untuk memasukkan properti src image
          },
          "user"
        );
        
        set fokus element input lompat 2 index agar tidak bertabrakan dengan image yang sudah di embed
        quillRef.current.setSelection(range.index + 2, "silent");
        quillRef.current.focus();
        handleShowModal();
      } catch (error) {
        errorNotification(`Upload File`, error);
      }
    }
  };

  // Penggunaan useMemo disini untuk memoize hasil function custom handlers yang dipanggil berulang kali agar tidak bentrok

  const modules = React.useMemo(
    () => ({
      toolbar: {
        container: [
          [{ font: [] }],
          [{ header: [2, 3, false] }],
          [{ align: [] }],
          ["bold", "italic", "underline"],
          [{ list: "ordered" }, { list: "bullet" }],
          ["link", "image", "video"],
          ["clean"],
        ],
      },
      history: {
        delay: 500,
        maxStack: 100,
        userOnly: true,
      },
      handlers: {
        image: handleShowModal, // customs handlers image untuk memanggil function handleShowModal
      },
    }),
    []
  );
  // daftarkan Quill instance module image
  Quill.register({ "formats/imageBlot": ImageBlot });

  return (
    <>
      <ReactQuill
        id={"richContent"}
        modules={modules}
        formats={[
          "header",
          "font",
          "bold",
          "italic",
          "underline",
          "list",
          "bullet",
          "align",
          "link",
          "image",
          "video",
        ]}
        placeholder={""}
        onChange={(e) => setContentValue(e)}
        value={contentValue}
        theme="snow"
      />
      <Modal
        size="sm"
        show={showModalImage}
        onClose={handleShowModal}
        title="Tambah Image"
        variant="clean"
      >
        <Modal.Body>
          <Form>
            <Form.Item
              label="Upload Image"
              extra={
                <>
                  Atau seret file ke area di atas <br />
                  Format .jpg or .jpeg
                </>
              }
            >
              <UploadPreview>
                <UploadDrag
                  name="upload"
                  beforeUpload={(e) => onUpload(e)}
                  accept="image/jpeg"
                  type="drag"
                >
                  <Button icon="plus">Upload Image</Button>
                </UploadDrag>
              </UploadPreview>
              {selectedImage.image && (
                <ListStyled isModal>
                  <li>
                    <div>
                      <strong>{selectedImage.fileName}</strong>
                      <span>{formatBytes(selectedImage.size)}</span>
                    </div>

                    <Button
                      iconOnly
                      icon="trash"
                      onClick={() => setSelectedImage(initialImage)}
                    />
                  </li>
                </ListStyled>
              )}
            </Form.Item>
            <Form.Item label="Image Title">
              <Input
                placeholder="Input Image Title"
                value={titleImage}
                onChange={(e) => onChangeInput("title", e)}
              />
            </Form.Item>
            <Form.Item label="Image ALT Title">
              <Input
                placeholder="Input Image ALT Title"
                value={altImage}
                onChange={(e) => onChangeInput("alt", e)}
              />
            </Form.Item>
          </Form>
        </Modal.Body>
        <Modal.Footer>
          <Button disabled={isFetchingUpload} onClick={handleShowModal}>
            Cancel
          </Button>
          <Button
            color="primary"
            loading={isFetchingUpload}
            onClick={uploadImage}
          >
            Upload
          </Button>
        </Modal.Footer>
      </Modal>
    </>
  );
};

export default RichContentEditor;

```
Buat File image.blot.tsx untuk mendaftarkan custom property image (alt, title, src)

```
import Quill from 'quill';

const BlockEmbed = Quill.import('formats/image');

class ImageBlot extends BlockEmbed {
  static create(val) {
    const node = super.create();
    node.setAttribute('alt', val.alt);
    node.setAttribute('title', val.title);
    node.setAttribute('src', val.url);
    return node;
  }

  static value(node) {
    return {
      alt: node.getAttribute('alt'),
      title: node.getAttribute('title'),
      url: node.getAttribute('src'),
    };
  }
}
ImageBlot.blotName = 'image';
ImageBlot.tagName = 'img';

export default ImageBlot;
```

seperti ini output yang dihasilkan 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i8mj5w75cpmuig4lqigj.png)