ใช้ Quill.js ร่วมกับ Rails 5.2

เมื่อก่อนผมใช้ TinyMCE เป็นหลักเวลาที่ต้องการ setup HTML text editor แต่ช่วงหลังผมเริ่มเปลี่ยนจาก TinyMCE แล้วหันมาใช้ Quill.js แทน ในหลายๆ โปรเจค ผมว่าสำหรับผู้ใช้แล้วมันใช้งานง่าย ดูดีพอๆ กัน แต่ในส่วนของลูกเล่นและ API นั้น Quill.js จะแตกต่างจาก TinyMCE ไปเลย ยกตัวอย่างเช่น ถ้าเราต้องการแก้ไขหรือค้นหาบางส่วนของเนื้อหา API ของ Quill.js จะให้เราระบุเลยว่าจะหาอะไรที่ตำแหน่งไหน โดย Quill.js จะมองเนื้อหาที่มันจะเอามาจัดการเป็น Delta ซึ่งเป็น concept ในการจัดการกับเนื้อหาของตัวเองโดยเฉพาะ ทำให้สามารถ implement ความสามารถของ text processor อย่าง undo, แทรกข้อความ หรือ cut/paste ได้แบบเนียนเลยทีเดียว สำหรับบทความนี้ ผมจะอธิบายถึงวิธีการเอา Quill.js มาใช้ร่วมกับ Rails อย่างเดียว ถ้าคุณสนใจฟีเจอร์อื่นๆ ของ Quill.js นั้นก็สามารถตามไปดูรายละเอียดได้จาก Official document ของ Quill.js ได้เลย

สร้าง Rails โปรเจคเพื่อลองใช้งานกับ Quill.js

ก่อนที่จะเริ่ม สมมติว่าเรามีโมเดลชื่อ Article ซึ่งเราต้องมี form ให้ผู้ใช้สำหรับเขียนบทความใหม่ และฟอร์มสำหรับแก้ไขบทความเดิม โดยเนื้อหาของบทความนั้น เราต้องการนำ Quill editor มาใช้แทนที่ text area แบบทั่วไป เรามาลองดูกันครับว่า Quill.js จะทำงานร่วมกับ Rails อย่างไร ซึ่งผมจะสร้างแอพตัวตัวอย่างชื่อ quilljs ขึ้นมา โดยเริ่มจากการใช้คำสั่ง

$ rails new quilljs –-webpack –-no-coffee –T

จากนั้นเตรียมโมเดล กำหนด routes เตรียมคอนโทลเลอร์และวิว สำหรับการสร้าง และแก้ไข Article ขึ้นมาให้เรียบร้อย ทั้งหมดนี้เป็นโค้ดพื้นฐานง่ายๆ คล้ายกับตัวอย่างก่อนๆ ซึ่งสามารถเข้าไปดูโค้ดอ้างอิงได้ใน repository บน Github ที่ผมอัพโหลดเอาไว้แล้ว

เราจะเริ่มดูจากโค้ดที่เป็น form สำหรับสร้างและแก้ไข Article ซึ่งผมแยกเป็นไฟล์ parital เอาไว้ ในที่นี้คือไฟล์ _form.html.erb แล้วแก้ไขโค้ดใหม่ให้เป็น

<%= form_with model: @article, id: "form-article" do |f| %>
 <div class="field--group">
  <%= f.label :title %>
  <%= f.text_field :title %>
 </div>
 <div class="field--group">
  <%= f.label :description %>
  <%= f.hidden_field :description %>
  <div id="quill"></div>
 </div>
 <div class="field--group">
  <%= f.submit %>
 </div>
<% end %>

จะเห็นว่าเราไม่ได้เรียกใช้ f.text_area สำหรับฟิลด์ description เหมือนที่ผ่านมา เพราะเราจะให้ Quill.js สร้าง HTML editor ให้ผู้ใช้เขียนเนื้อหาของบทความแทน โดยเราใส่แท็ก div ที่มี id เป็น quill เอาไว้เพื่อให้ Quill.js รู้ว่าตำแหน่งนี้เป็นจุดที่ Quill.js จะเอา Quill editor มาวางซึ่งจะใช้เพื่อการแสดงผล

ส่วนเวลาที่เราจะโฟสข้อมูลขึ้นไปบน server เราจะบอกให้ Quill.js คัดลอก HTML string จาก Quill editor ที่ว่า มาใส่ในช่องของ <%= f.hidden_field :description, id: “article_description” %> ซึ่ง HTML string จะถูกส่งไปยัง server ในฐานะของการเป็นข้อมูลของฟิลด์ description ทำให้เราสามารถ permit และนำค่าของมันไปใช้ในแอกชั่น create และ update ใน ArticleController ได้ต่อไป

โหลด Quill.js

ระบุว่าเราต้องการเพิ่ม Quill.js เข้ามาไว้ในไลบรารี่

$ yarn add quill@1.3.6

ในส่วนของโค้ด javascript ที่ทำการเรียกใช้ Quill editor ขึ้นมาใช้นั้น ผมอ้างอิง document ของ Quill.js แล้วนำมาเขียนโค้ดก็จะได้ประมาณนี้ โดยโค้ดจะอยู่ในไฟล์ app/javascript/components/article/article.js ซึ่งผมจะเรียกมาจากไฟล์ app/javascript/pack.js อีกที

import Quill from 'quill';
import 'quill/dist/quill.core.css';
import 'quill/dist/quill.snow.css';

document.addEventListener('turbolinks:load', function(){
 // Setup and manipulate Quill editor
 const quillEditor = document.getElementById('quill')
 if(quillEditor == null) { return }

 var quill = new Quill('#quill', {
   modules: {
     toolbar: [
       [ { font: ['sans-serif', 'monospace']}, { header: [1, 2, 3, false] }],
       ['bold', 'italic', 'underline'],
       ['link', 'blockquote'],
       [{ list: 'ordered' }, { list: 'bullet' }]
     ]
   },
   placeholder: 'เขียนบทความที่นี้...',
   theme: 'snow'
 })

จากโค้ดข้างต้นเรากำหนดให้โหลด Quill editor ลงไปใน tag id ชื่อ quill ถ้าไม่มี tag id ชื่อนี้ก็ให้ return ออกมาเลยไม่ต้องทำอะไรต่อ จากนั้นก็ initialize โดยเรียก new Quill() ก็เป็นอันเรียบร้อย

ณ จุดนี้ถ้าลองรันแอพดู แล้วไปที่หน้า localhost:3000/articles/new จะเห็นว่ามี Quill editor ปรากฏที่ตำแหน่งของ description แล้ว

iแต่พอลองเขียนเนื้อหาลงใน Quill editor แล้วกด submit เราจะเห็นว่าเนื้อหาที่เราเขียนลงไป มันไม่ถูกส่งมากับ form ไปยัง server อย่างที่เราคาด ถ้าไล่ log ใน rails server windows ดูจะเห็นว่าไม่มีข้อมูลของตัวแปร description ส่งมาจาก form อย่างทีว่าไว้จริงๆ

Started POST "/articles" for 127.0.0.1 at 2018-09-16 11:21:12 +0700
Processing by ArticlesController#create as JS
 Parameters: {"utf8"=>"✓", "authenticity_token"=>"LnQsUk5Or394bPWewSVidwKD/uV+56eScYeQAkylznkxM3H45ewcFCGF6EScH5iaDpaHMC0RYW1yBFEnFbg==", "article"=>{"title"=>"", "description"=>""}, "commit"=>"Create Article"}

นั่นเป็นเพราะ Quill editor ที่เราเขียนเนื้อหาลงไป ไม่ได้ถูกกำหนด name เอาไว้ ดังนั้น input ในส่วนนี้จึงไม่ได้ถูกส่งไปกับ form จริงๆ แล้วเราสามารถเขียนโค้ด javascript เพื่อระบุ name ให้กับ input ฟิลด์ของ Quill editor แต่มันจะเป็นปัญหาตอนที่เราต้องการโหลดข้อมูลของ description ขึ้นมากับ Quill editor ใน form แก้ไข

ส่ง HTML String จาก Quill editor ไปกับ field hidden เพื่ออัพเดตฟิลด์ description

เพื่อแก้ปัญหานี้ เราจะเพิ่ม input[type=hidden] เข้าไปโดยกำหนดให้มีค่าของ name ที่สอดคล้องกับตัวแปร description ที่เราต้องการส่งไปยัง server พร้อมกับ form นั้นคือ name=article[description] โดยเราจะเขียนโค้ด javascript เพื่อ copy เนื้อหาทั้งหมดจาก Quill editor (HTML string) ไปใส่เป็นค่า value ของ input[type=hidden] ตัวนี้ ซึ่งจะทำให้เนื้อหาของเราสามารถส่งไปกับ form ภายใต้ตัวแปร description ได้

ส่วนเวลาที่ต้องการแก้ไข article เราก็จะทำกลับกันคือ เราก็จะ copy ค่าของ input[type=hidden] (ซึ่ง render มาจาก server) แล้วนำไปแปะไว้ที่ Quill editor ดังนั้นเมื่อ ผู้ใช้ไปที่หน้า edit ก็จะเห็นเนื้อหาล่าสุดอยู่ใน Quill editor อย่างที่ควรจะเป็นนั่นเอง

โค้ดในการ copy ค่าของ input[type=hidden] ไปมา จะอยู่ในไฟล์ app/javascript/components/article/article.js ตามที่อธิบายไว้ข้างต้นมีหน้าตาประมาณนี้

 let article_content = document.getElementById("article_description")
 if(article_content != "") {
   quill.root.innerHTML = article_content.value
 }

 let formArticle = document.getElementById("form-article")
 formArticle.addEventListener('submit', function(e){
   article_content.value = quill.root.innerHTML
   return true
 })

ให้สังเกตุว่าในขณะเราอยู่ในหน้าฟอร์ม ที่มีการใช้ Quill editor ถ้าเรากดลิ้งไปยังหน้าอื่นแล้วกด Back กลับมาหน้าเดิมอีกที เราจะเห็นว่ามันมี Toolbar ของ Quill editor เพิ่มเข้ามาทุกครั้งที่กด Back กลับมา นั่นเป็นเพราะว่า พอกด Back หน้าเพจจะถูกโหลดมาจาก cache ที่ Turbolinks เก็บเอาไว้ เมื่อโค้ดที่อยู่ภายในอีเว้นท์ turbolinks:load จะถูกเรียกขึ้นมารัน จึงเกิด Toolbar ส่วนเกินอันใหม่เพิ่มขึ้นมา เพื่อให้ Quill editor แสดงผลได้อย่างถูกต้องไม่มี Toolbar เกินมา เราจะบอกให้ Turbolinks เอา Toolbar ของ Quill ออกไปก่อนที่จะเก็บ cache เอาไว้ Turbolinks เพื่อที่จะทำงาน เคลียร์ Toolbar ทุกครั้งเวลากด Back โดยเพิ่มโค้ดเอาไว้ในอีเว้นท์ turbolinks:before-cache ดังนี้

document.addEventListener('turbolinks:before-cache', function(){
 const quillToolbar = document.querySelector('.ql-toolbar')
 if(quillToolbar != null) { quillToolbar.remove() }
})

สรุป

จะเห็นว่าการนำ Quill.js มาใช้ร่วมกับโปรเจค Rails นั้นก็ไม่ได้ยากอะไรมาก จะมี trick นิดหน่อยก็ตรงที่เราต้องใช้ input[type=hidden] สำหรับรับส่งข้อมูลของตัวแปรที่เราใช้เก็บเนื้อหาของ Quill editor ระหว่างการ submit ข้อมูลขึ้นมายัง server และการแสดงผลข้อมูลเวลาที่เราจะ edit ผ่านทาง Quill editor แค่นั้นเอง

แชร์โพสได้จากลิ้งด้านล่าง ขอบคุณครับ