ในโพสต์นี้ เราจะ implement ActionText table editor พื้นฐานสำหรับแอปพลิเคชัน Rails ของคุณ เราจะเรียนรู้ว่า:
- ActionText และ Trix จัดการ attachments อย่างไร
- การ implement
Attachabletype ของตัวเอง และใช้ประโยชน์จากมันเพื่อสร้าง table editor พื้นฐาน - Turbo Frames สามารถใช้ในการ edit table ได้อย่างไร
- Turbo ช่วยและขัดขวางพร้อมกัน
บทความนี้ได้รับแรงบันดาลใจจากโพสต์บล็อก 'Adding Tables to ActionText With Stimulus.js' ที่ยอดเยี่ยมจากปี 2020 นั่นเขียนก่อน Turbo ซึ่งเราคาดว่าจะทำให้เรื่องง่ายขึ้นมาก
ลองเริ่มกันเถอะ!
ActionText Attachments ใน Rails 101
หมายเหตุ: การสาธิตนี้สมมติว่ามีความเข้าใจบางอย่างเกี่ยวกับ Trix และ Turbo Frames คุณอาจพบโพสต์ 'Get Started with Hotwire in Your Ruby on Rails App' ของเรามีประโยชน์ในการเรียนรู้พื้นฐาน Hotwire และ Turbo Frames
คุณสามารถติดตามการสาธิตโค้ดกับ GitHub repo นี้
ตามที่อธิบายในเอกสาร ActionText:
Action Text นำ rich text content และ editing มาให้ Rails มันรวม Trix editor ที่จัดการทุกอย่างจาก formatting ไปจนถึง links, quotes, lists, embedded images และ galleries
ในระดับสูง attachments เป็นส่วนหนึ่งของ document model ของ ActionText พวกเขา render custom templates สำหรับทรัพยากรใดๆ ที่สามารถ resolve ได้ด้วย Signed Global ID (SGID) กล่าวอีกนัยหนึ่ง ActionText จัดเก็บการอ้างอิงถึง SGID เฉพาะเป็น element <action-text-attachment>:
ทุกครั้งที่ ActionText พบ element นี้ มันจะเรียกเมธอด to_attachable_partial_path บนทรัพยากรที่เกี่ยวข้อง โดยค่าเริ่มต้น เมธอดนี้จะ delegate ไปยัง to_partial_path
ดังนั้น เพื่อเป็นตัวอย่างเบื้องต้น นี่คือลักษณะของการแทนที่ของ Table ใน ActionText เมื่อ render กลับเป็น HTML:
เพื่อปฏิบัติตาม ActionText Attachment API คลาสต้องทำเพียงสองอย่าง:
- Implement
to_sgidโดย includeGlobalID::Identificationโดยค่าเริ่มต้น ทุกActiveRecord::Basedescendants ทำสิ่งนี้แล้ว - Include โมดูล
ActionText::Attachable
โมดูล ActionText::Attachable ให้วิธีมาตรฐานในการแปลงโมเดลใดๆ ไปและจาก SGID ผ่านเมธอด attachable_sgid และ from_attachable_sgid เราจะใช้ประโยชน์จากนี้ภายหลัง
มันยังให้ convenience accessors สำหรับ attachment metadata เช่น file size และ name รวมถึง content type
สุดท้าย มันให้ default locations สำหรับ partials ที่ใช้ render attachment ใน editor และ rich text views
การเพิ่ม Table Model
เราจะใช้ประโยชน์จาก ActionText Attachment API เพื่อ implement table solution ของเรา สำหรับสิ่งนี้ เราต้องสร้าง custom model ที่จับข้อมูล table ของเราและ include Attachable เราจะใช้ JSON(B) column แบบง่ายเพื่อเก็บ two-dimensional array สำหรับข้อมูล table
เพื่อเริ่มการสำรวจ เราสร้างแอป Rails ใหม่พร้อม ActionText enabled:
เพราะวันนี้ไม่คิดสร้างสรรค์ สร้าง scaffold Article model พร้อม title และ rich text content:
ดูโจ๋! คำสั่ง install ข้างต้นสร้าง migration CreateActionTextTables ดังนั้นเราต้อง rename เป็น CreateActionTextTablesTable นอกจากนี้ เราจะให้ default เป็นตาราง 2x2 โดยใช้ null: false, default: [["", ""], ["", ""]]
เพิ่ม Table ให้กับ Rails ActionText Model
ก่อนที่เราจะต่อไปกับการเพิ่ม table เข้า rich text เราต้อง patch toolbar ของ Trix:
ที่นี่ เรา append ปุ่มเข้าไปใน toolbarElement ของ Trix ด้วยตนเอง การ wiring นี้กับ Stimulus controller trix-table (ที่เรายังไม่ได้สร้าง) จะ insert table เข้า document ให้ปุ่มนี้มี SVG สวยงามเป็น content ใน CSS และตั้งค่า table styles พร้อมกัน:
ตามที่คุณเห็น เราเพิ่มมันเข้า group "file-tools" สำเร็จ:

ตอนนี้มากลับไปที่การเพิ่มและจัดการ tables ด้วยความช่วยเหลือของ Turbo สำหรับสิ่งนี้ เราจะต้องมี controller พร้อม action create ก่อน:
Action นี้สามารถยืมมาจากโพสต์บล็อก 'On Rails' ที่เราอ้างอิงในบทนำ มันสร้าง JSON ที่จำเป็นในการ insert attachment ฝั่ง client: รวมถึง SGID และ content ที่ render จาก partial editor ซึ่งเราจะเห็นภายหลัง
เราเพิ่ม resourceful table routes ที่เกี่ยวข้องใน configuration:
ตอนนี้มาถึงเวลาที่จะกระโดดเข้าสู่ deep end: เราต้องสร้าง table model ของเรา ก่อนอื่น ให้ include ActionText::Attachable และกำหนด partial paths ที่เกี่ยวข้อง:
สังเกตว่าเรายังไม่ได้กำหนดว่า table content จะจัดเก็บอย่างไร เนื่องจากเรา declare เป็น JSON(B) column ในฐานข้อมูล เราจึงอิสระที่จะเลือก format ใดๆ ห่างจากโพสต์บล็อกที่อ้างอิงไปบ้าง เราจะไปกับ two-dimensional array ดังนั้นเราสามารถ loop nested ผ่าน content ได้อย่างง่ายดาย:
Partial ข้างต้นจะ render ทุกครั้งที่ถูกขอโดย ActionView ตัวอย่างเช่น ต่อไป เราต้องออกแบบ partial editor สำหรับใช้ inline ใน Trix:
ความแตกต่างเดียว ตามที่คุณน่าจะสังเกต คือตอนนี้เรา wrap มันใน Turbo Frame ใช้ SGID เป็น DOM id นอกจากนี้ เราให้ row และ column indexes กับ separator blocks และเตรียมพร้อมสำหรับ inline editing โดยทำให้ inner DIV contenteditable — เราจะไปถึงนั้นภายหลัง
เราจะเชื่อมปุ่ม table ของ toolbar เข้ากับ server-side controller action ที่เราเพิ่งเขียน ตอนนี้ เราต้องนำ library request.js ของ Rails เข้าโปรเจกต์ ไลบรารีนี้จะช่วยเราจัดการ post requests จาก client รวมถึง CSRF-tokens ที่เหมาะสม ฯลฯ:
สร้าง Trix Table Stimulus Controller ใหม่
ตอนนี้ทุกอย่างตั้งค่าแล้ว เรามาสร้าง trix-table Stimulus controller ใหม่ ในมัน เราจะ implement action attachTable ที่ถูกอ้างอิงโดยปุ่ม toolbar ของเรา:
มันจะ POST ไปที่ route create ของ table insert JSON response เป็น Trix attachment นี้อีกครั้งยืมมาจากโพสต์บล็อก OnRails แลกเปลี่ยน calls rails-ujs ที่ deprecated สำหรับ library request.js ใหม่
ตอนนี้เราต้องใช้ controller นี้ในแอปของเราจริง โดยเพิ่มมันเข้า markup ของ form:
ความสวยงามของ Stimulus.js คือการเพิ่มแค่สอง data attributes ให้ element form ทำให้ได้ผลลัพธ์ที่ต้องการ เราตอนนี้สามารถเพิ่ม tables ให้ content ของ article ได้ด้วยการคลิกปุ่มเดียว:

การจัดการ Table ผ่าน Turbo Frames
ตอนนี้เราสร้าง table attachments ได้แล้ว มาเปลี่ยนโฟกัสไปที่การจัดการ content ตามที่พบ Turbo Frames เกือบ เหมาะสมโดยธรรมชาติที่นี่
เพิ่มและลบ Table Rows และ Columns
เพื่อเพิ่มและลบ table rows และ columns เราสร้าง mini-toolbar ประกอบด้วยสี่ปุ่ม หนึ่งสำหรับแต่ละ operation ใช้ button_to helper และตั้ง URL ไปที่ route update สำหรับ table เฉพาะ ให้เพิ่ม operation ที่เราต้องการทริกเกอร์เป็นพารามิเตอร์เพิ่มเติม:
ในทางกลับกัน เราต้องเพิ่ม controller action(s) ที่เกี่ยวข้องใน TablesController สังเกตว่า action update delegate actions นั้นไปยัง model
หลังจากการเปลี่ยนแปลงโครงสร้าง table ถูกบันทึก เรา redirect ไปที่ edit view ของ table มัน render partial editor เดียวกัน ซึ่งมี side-effect คืออ้างอิง Turbo Frame เดียวกัน ดังนั้น Turbo จึงตรวจจับ frame ที่ตรงกันและแทนที่หนึ่งด้วยอีกหนึ่ง
ตอนนี้เราต้อง implement คำสั่งที่ขาดหายไปบนโมเดล Table
น่าสังเกต เนื่องจากโครงสร้างข้อมูลง่ายของเราเป็น two-dimensional array เมธอด add/remove<sub>column</sub>/row เป็นเพียง proxies เพื่อแก้ไข column และ row count เมื่อสิ่งนี้พร้อม เราสามารถเปลี่ยนโครงสร้าง table ด้วยการคลิกปุ่ม:

แก้ไขเนื้อหาของ Table Cells
นอกจากการเปลี่ยนจำนวน columns และ rows เรายังต้องการแก้ไขเนื้อหาของ cells เพื่อบรรลุสิ่งนี้ เราจะพิงพาอีกครั้งจากโพสต์บล็อกที่อ้างอิง และสร้าง Stimulus table editor controller
เมธอด updateCell จะออก PATCH request ทุกครั้งที่ cell ถูกแก้ไข ส่ง row และ column index เป็นพารามิเตอร์ ตอนนี้สิ่งที่เราต้องทำคือเชื่อมมันกับ DOM:
Server-side TablesController ก็ต้องมีวิธีจัดการ operation นี้ โชคดีที่ทำได้ง่ายใน proof of concept ที่เราลดรูปโดยเพิ่ม branch อีกหนึ่งใน condition เรายังทำให้แน่ใจว่า action update สามารถจัดการ JSON-type requests ได้ แม้จะเพียงคืนค่าอ็อบเจกต์ว่าง
สังเกตว่าใน production app ฉันแนะนำให้คุณเลือกกลยุทธ์ที่แตกต่างสำหรับการ sanitize operation แทน condition if/elsif/else ฉันอาจจะใช้ Mediator หรือ Proxy ในกรณีนี้
ข้อจำกัดของ Trix ใน Ruby
จนถึงจุดนี้ ฉันสมมติว่าบันทึกนี้มีความสมเหตุสมผลสมบูรณ์ แต่ฉันได้ละเลยรายละเอียดสำคัญหนึ่ง ขณะที่เรา persist underlying database model ได้ดี เราไม่ได้ sync มันกับ internal shadow representation ของ Trix นั่นคือเหตุผลที่ table snap กลับไปที่ representation ที่จัดเก็บไว้ก่อนหน้านี้เมื่อเรา focus ออกจากมัน:

หากเรา refresh หน้าตอนนี้ เนื้อหาที่เพิ่มจะปรากฏ เพราะ document ของ Trix ถูก initialize ใหม่
ฉันปักหมุดปัญหานี้ไว้ที่จุดที่ Trix sync internal document เมื่อ selection เปลี่ยน มัน unfurl มันจาก shadow element ที่นี่
ฉันลอง hook เข้า event turbo:submit และ prevent sync เพียงเมื่อ blur table แต่ solutions ที่ฉันหามีทั้งหมดดู hairy และขึ้นอยู่กับ internal API มาก
วิธีที่ Turbo-esque ที่สุดในการจัดการสิ่งนี้ ฉันเดาว่าจะเป็นการ wrap whole form ใน Turbo Frame ที่ eager-loaded และบอกให้มัน reload ทุกครั้งที่ content ของ Trix เปลี่ยน
สิ่งแบบนี้ควรทำงาน:
หากคุณล้อม form ไว้ใน Turbo Frame ที่คุณ load จาก src:
แนวทางนี้ทำงานได้กับ base records ที่ persisted แล้วเท่านั้น
คำเตือนสุดท้ายเกี่ยวกับ Trix
Proof of concept ที่เราสร้างใช้ server-rendered HTML เพื่อกำจัดความซับซ้อนเพิ่มเติมของการ serialize tables ไป JSON และฟัง JavaScript events มัน portable ไปยังการติดตั้ง ActionText ใดๆ และสามารถ extract ไปเป็น gem ได้ง่าย
มีข้อเสียสองสามอย่าง อย่างเด่นชัดที่สุดคือ re-syncing ที่จำเป็นกับ document model ของ Trix อาจมีสถานการณ์ที่ workaround ที่เสนอทำงานได้ และบางอย่างที่ไม่ได้ จนกว่า Trix จะมี Turbo-compatible interface ไม่มีทางหลบหนี
ข้อจับที่สองคือ มันไม่ใช้ functionality undo ของ Trix (แต่สิ่งนี้จริงสำหรับ Trix attachment ใดๆ) เช่นเดียวกัน จะเป็นเรื่องฉลาดที่จะรอ upstream changes แทนที่จะ tweak internal API
สรุป
ในโพสต์นี้ เราเริ่มต้นด้วยการดูพื้นฐานของ ActionText Attachments แล้วเพิ่ม table ให้ ActionText model ก่อน tweak มันด้วย Turbo Frames สุดท้ายเรา mention ข้อจำกัดบางอย่างของการใช้ Trix
เนื่องจาก Trix v2 กำลังพัฒนา มีการแปลจาก CoffeeScript ไป plain modern JavaScript ตอนนี้จะเป็นเวลาที่ดีในการแก้ Turbo compatibility ปัจจุบัน ขอบเขตของ wrapper ที่อาจเป็นอย่างไรกำลังเกินความสามารถของฉัน แต่ดูเหมือน window of opportunity
ขอให้เขียนโค้ดสนุกสนาน!
P.S. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่เผยแพร่ สมัครรับจดหมายข่าว Ruby Magic และจะไม่พลาดโพสต์ใดๆ!