এই পোস্টে আমরা আপনার Rails অ্যাপ্লিকেশনের জন্য একটি বেসিক ActionText টেবিল এডিটর ইমপ্লিমেন্ট করব। আমরা শিখব:
- ActionText এবং Trix কীভাবে attachments হ্যান্ডেল করে
- আমাদের নিজস্ব
Attachableটাইপ ইমপ্লিমেন্ট করা এবং এটি ব্যবহার করে একটি বেসিক টেবিল এডিটর বানানো - Turbo Frames কীভাবে টেবিল এডিট করতে ব্যবহার করা যায়
- Turbo কীভাবে সাহায্য করে এবং একই সাথে বাধা দেয়
এই আর্টিকেল ২০২০ সালের excellent 'Adding Tables to ActionText With Stimulus.js' ব্লগ পোস্ট থেকে অনুপ্রাণিত। Turbo আসার আগে লেখা হয়েছিল, যা বিষয়গুলো অনেক সহজ করে দিয়েছে।
Rails-এ ActionText Attachments ১০১
নোট: এই ডেমো কিছু Trix এবং Turbo Frames বুঝতে হবে। 'Get Started with Hotwire in Your Ruby on Rails App' পোস্ট হেল্পফুল হতে পারে বেসিক শিখতে।
GitHub repo-র সাথে ফলো করুন।
ActionText ডকুমেন্টেশন অনুযায়ী:
Action Text Rails-এ রিচ টেক্সট কন্টেন্ট এবং এডিটিং আনে। এতে Trix এডিটর থাকে যা ফরম্যাটিং থেকে লিংক, কোট, লিস্ট, এম্বেডেড ইমেজ এবং গ্যালারি হ্যান্ডেল করে।
হাই লেভেলে, attachments ActionText-এর ডকুমেন্ট মডেলের অংশ। সেগুলো কাস্টম টেমপ্লেট রেন্ডার করে যেকোনো রিসোর্সের জন্য যা Signed Global ID (SGID) দিয়ে রিসলভ করা যায়। অন্য কথায়, ActionText একটি রেফারেন্স স্টোর করে একটি নির্দিষ্ট SGID-কে <action-text-attachment> এলিমেন্ট হিসেবে:
<action-text-attachment sgid="BAh7CEkiCGdpZAY6BkVUSSI..."></action-text-attachment>
যখন ActionText এ্যাটাচমেন্ট এলিমেন্ট এনকাউন্টার করে, এটি respective রিসোর্সের ওপর to_attachable_partial_path মেথড কল করে। ডিফল্টলি এই মেথড to_partial_path-এ ডেলিগেট করে।
প্রিভিউ হিসেবে, আমাদের Table-র ActionText-এ রিপ্রেজেন্টেশন HTML-এ রেন্ডার হলে কীভাবে দেখাবে:
<action-text-attachment sgid="..."></action-text-attachment>
ActionText Attachment API-এর সাথে কনফর্ম করতে একটি ক্লাসকে শুধু দুটি কাজ করতে হয়:
to_sgidইমপ্লিমেন্ট করুনGlobalID::Identificationইনক্লুড করে। ডিফল্টলি সবActiveRecord::Basedescendents এটি করে।ActionText::Attachableমডিউল ইনক্লুড করুন।
ActionText::Attachable মডিউল ক্যাননিক্যাল পদ্ধতি দেয় যেকোনো মডেলকে SGID-এ কনভার্ট করার জন্য attachable_sgid এবং from_attachable_sgid মেথড দিয়ে। আমরা পরে এটা ব্যবহার করব।
এছাড়া attachment metadata-এর জন্য convenience accessors দেয় (ফাইল সাইজ, নাম, কন্টেন্ট টাইপ)। এবং এডিটর ও রিচ টেক্সট ভিউতে আট্যাচমেন্ট রেন্ডার করার জন্য ডিফল্ট partial লোকেশন দেয়।
Table Model যোগ করুন
ActionText-এর Attachment API ব্যবহার করে আমাদের টেবিল সলিউশন ইমপ্লিমেন্ট করব। এর জন্য একটি কাস্টম মডেল তৈরি করতে হবে যা আমাদের টেবিলের ডেটা ক্যাপচার করবে এবং Attachable ইনক্লুড করবে। টেবিল ডেটা ধারণ করার জন্য একটি সিম্পল JSON(B) কলাম ব্যবহার করব (two-dimensional array)।
নতুন Rails অ্যাপ ActionText-সহ
$ rails new table_editor --css tailwind
$ rails g scaffold Article title:string content:rich_text
$ rails db:migrate
গোটচা! ইনস্টল কমান্ড একটি CreateActionTextTables মাইগ্রেশন তৈরি করলো, আমাদের এটাকে CreateActionTextTablesTable-এ রিনেম করতে হবে। অডিশনালি, ডিফল্ট ২x২ টেবিল সেট করব null: false, default: [["", ""], ["", ""]] দিয়ে।
$ rails g migration CreateActionTextTablesTable content:jsonb
# মাইগ্রেশন এডিট করুন default সেট করার জন্য
$ rails db:migrate
Rails ActionText মডেলে টেবিল যোগ করুন
আকচুয়ালি টেবিল রিচ টেক্সটে যোগ করার আগে, Trix-এর টুলবার প্যাচ করতে হবে:
# app/javascript/controllers/trix_toolbar_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
const button = document.createElement("button")
button.type = "button"
button.className = "trix-button"
button.title = "Insert Table"
button.dataset.action = "trix-table#attachTable"
button.innerHTML = `` // SVG আইকন
this.element.querySelector("[data-trix-button-group=file-tools]").appendChild(button)
}
}
এখানে আমরা ম্যানুয়ালি Trix-এর toolbarElement-এ একটি বাটন অ্যাপেন্ড করছি। এটি trix-table Stimulus কন্ট্রোলারের সাথে ওয়ায়ার্ড (যেটা এখনো বানানো নেই) যাতে টেবিল ডকুমেন্টে ইনসার্ট করা যায়। CSS-এ বাটনের জন্য SVG কন্টেন্ট এবং টেবিল স্টাইল সেটআপ করুন:
/* app/assets/stylesheets/trix_table.css */
.trix-button::before { content: ""; /* SVG background */ }
table { border-collapse: collapse; width: 100%; }
td, th { border: 1px solid #ddd; padding: 8px; }
"file-tools" গ্রুপে সফলি অ্যাড হয়েছে:
Table Model বানান
ActionText::Attachable ইনক্লুড করুন এবং relevant partial paths ডিফাইন করুন:
class Table < ApplicationRecord
include ActionText::Attachable
include GlobalID::Identification
def to_attachable_partial_path
"tables/attachable"
end
def attachable_content_type
"text/html"
end
end
টেবিল কন্টেন্ট কীভাবে স্টোর হবে সেটা ডিক্লেয়ার করি নাই। JSON(B) কলাম ডিক্লেয়ার করা হয়েছে, তাই যেকোনো ফরম্যাট চয়িন করুন। ব্লগ পোস্ট থেকে একটু deviate করে, two-dimensional array যাই। নেস্টেড লুপ দিয়ে content ইটারেট করুন:
<%=# app/views/tables/_attachable.html.erb %>
<table>
<% content.each do |row| %>
<tr>
<% row.each do |cell| %>
<td><%= cell %></td>
<% end %>
</tr>
<% end %>
</table>
এই partial যেকোনো সময় ActionView রিকোয়েস্ট করলে রেন্ডার হবে। এডিটর-এ ইনলাইন ব্যবহারের জন্য editor partialও লাগবে:
<%=# app/views/tables/_editor.html.erb %>
<turbo-frame id="<%= attachable_sgid %>">
<table>
<% content.each_with_index do |row, row_index| %>
<tr>
<% row.each_with_index do |cell, col_index| %>
<td>
<div contenteditable="true"
data-row-index="<%= row_index %>"
data-col-index="<%= col_index %>">
<%= cell %>
</div>
</td>
<% end %>
</tr>
<% end %>
</table>
</turbo-frame>
এখানে পার্থক্য: Turbo Frame দিয়ে wrapped (SGID হিসেবে DOM id), row/col index দেওয়া, এবং inline editing-এর জন্য inner DIV contenteditable।
Rails-এর request.js লাইব্রেরি প্রজেক্টে আনার জন্য (CSRF tokens সহ POST রিকোয়েস্ট এডমিনিস্টার করতে):
$ bin/importmap pin @rails/request.js
নতুন Trix Table Stimulus Controller বানান
এখন সব সেটআপ হয়ে গেলে, নতুন trix-table Stimulus controller বানান। এতে attachTable অ্যাকশন ইমপ্লিমেন্ট করুন যা টুলবার বাটন রেফারেন্স করেছে:
# app/javascript/controllers/trix_table_controller.js
import { Controller } from "@hotwired/stimulus"
import { post } from "@rails/request.js"
export default class extends Controller {
attachTable() {
post("/tables", {
responseKind: "json",
body: JSON.stringify({ table: { content: [["", ""], ["", ""]] } })
}).then(response => {
if (response.ok) {
const json = response.json
this.element.editor.insertAttachment(json.sgid, json.content)
}
})
}
}
এটি টেবিলের create route-এ POST করবে, JSON রেসপন্সকে Trix attachment- হিসেবে ইনসার্ট করবে। OnRails ব্লগ পোস্ট থেকে বরো করা, deprecated rails-ujs কলস request.js দিয়ে এক্সচেঞ্জ করা।
এই কন্ট্রোলার অ্যাপে ব্যবহার করতে ফর্মের মার্কআপে যোগ করুন:
<%= form_with model: @article do |form| %>
<%= form.rich_text_area :content, data: { controller: "trix-toolbar trix-table" } %>
<% end %>
Stimulus.js-এর সুন্দরতা: ফর্ম এলিমেন্টে শুধু দুটি data attribute যোগ করলেই ফলাফল। একক বাটন ক্লিকে আর্টিকেল কন্টেন্টে টেবিল যোগ করা যায়:
Turbo Frames দিয়ে টেবিল ম্যানিপুলেট করুন
টেবিল আট্যাচমেন্ট তৈরি করার পর, কন্টেন্ট ম্যানিপুলেশনে ফোকাস করি। Turbo Frames almost একটি ন্যাচারাল ফিট এখানে।
টেবিল রো এবং কলাম অ্যাড/ডিলিট করুন
মিনি-টুলবার বানান চারটি বাটন দিয়ে (প্রতিটি অপারেশনের জন্য একটা)। button_to হেল্পার ব্যবহার করুন, URL সেট করুন টেবিলের update route-এ। অতিরিক্ত প্যারামিটার হিসেবে অপারেশন পাস করুন:
<%= button_to "Add Row", table_path(table, operation: "add_row"), method: :patch, data: { turbo_frame: attachable_sgid } %>
<%= button_to "Remove Row", table_path(table, operation: "remove_row"), method: :patch, data: { turbo_frame: attachable_sgid } %>
<%= button_to "Add Column", table_path(table, operation: "add_column"), method: :patch, data: { turbo_frame: attachable_sgid } %>
<%= button_to "Remove Column", table_path(table, operation: "remove_column"), method: :patch, data: { turbo_frame: attachable_sgid } %>
TablesController-এ respective controller action(s) যোগ করুন। update অ্যাকশন অপারেশনগুলো মডেলে ডেলিগেট করে:
def update
case params[:operation]
when "add_row"
@table.add_row
when "remove_row"
@table.remove_row
when "add_column"
@table.add_column
when "remove_column"
@table.remove_column
end
if @table.save
redirect_to edit_table_path(@table)
else
render :edit
end
end
টেবিল স্ট্রাকচার চেঞ্জ সেভ হওয়ার পর, টেবিলের edit view-এ রিডাইরেক্ট করুন। এটি একই editor partial রেন্ডার করে, যার side-effect: একই Turbo Frame রেফার করে। তাই Turbo ম্যাচিং ফ্রেম ডিটেক্ট করে এবং একটিকে অন্যটির সাথে সাবস্টিটিউট করে।
Table মডেলে missing commands ইমপ্লিমেন্ট করুন:
class Table < ApplicationRecord
# ...
def add_row
self.content << Array.new(content.first.size, "")
end
def remove_row
self.content.pop if content.size > 1
end
def add_column
content.each { |row| row << "" }
end
def remove_column
content.each { |row| row.pop if row.size > 1 }
end
end
বাটন ক্লিক দিয়ে টেবিলের স্ট্রাকচার চেঞ্জ করা যায়:
টেবিল সেল কন্টেন্ট এডিট করুন
রো/কলাম সংখ্যা চেঞ্জ করার পাশাপাশি, সেল কন্টেন্টও এডিট করতে চাই। Stimulus table editor controller তৈরি করুন:
# app/javascript/controllers/table_editor_controller.js
import { Controller } from "@hotwired/stimulus"
import { patch } from "@rails/request.js"
export default class extends Controller {
updateCell(event) {
const rowIndex = event.target.dataset.rowIndex
const colIndex = event.target.dataset.colIndex
const content = event.target.innerText
patch(`/tables/${this.element.dataset.tableId}`, {
body: JSON.stringify({
table: {
operation: "update_cell",
row: rowIndex,
column: col_index,
content: content
}
})
})
}
}
DOM-এ কানেক্ট করুন:
<div contenteditable="true"
data-controller="table-editor"
data-action="blur->table-editor#updateCell"
data-row-index="<%= row_index %>"
data-col-index="<%= col_index %>"
data-table-id="<%= attachable_sgid %>">
<%= cell %>
</div>
Server-side TablesController-এ এই অপারেশন হ্যান্ডেল করার জন্য আরেকটি branch যোগ করুন। JSON রিকোয়েস্ট হ্যান্ডেল করতে update অ্যাকশন সক্ষম করুন (এখানে শুধু empty object রিটার্ন):
def update
case params[:operation]
when "add_row", "remove_row", "add_column", "remove_column"
# ... existing code
when "update_cell"
row = params[:table][:row].to_i
col = params[:table][:column].to_i
@table.content[row][col] = params[:table][:content]
@table.save
head :ok
end
end
নোট: প্রোডাকশন অ্যাপে if/elsif/else কন্ডিশনের বদলে Mediator বা Proxy প্যাটার্ন ব্যবহার করা ভালো।
Ruby-এ Trix-এর Limitations
এতক্ষণ ধরে অ্যাকাউন্ট ঠিক লাগছে, কিন্তু একটি ক্রিটিক্যাল ডিটেইল বাদ দিয়েছি। আন্ডারলাইং ডেটাবেস মডেল পারসিস্ট হচ্ছে ঠিকমতো, কিন্তু Trix-এর ইন্টারনাল শ্যাডো রিপ্রেজেন্টেশনের সাথে সিঙ্ক হচ্ছে না। তাই যখন টেবিল থেকে ফোকাস আউট করি, সেটা previously stored representation-এ snap back করে:
পেজ রিফ্রেশ করলে অ্যাডেড কন্টেন্ট দেখাবে, কারণ Trix-এর ডকুমেন্ট fresh initialize হয়।
এই সমস্যাটি পিনপয়েন্ট করা গিয়ে: Trix সিলেকশন চেঞ্জ হলে ইন্টারনাল ডকুমেন্ট সিঙ্ক করে। শ্যাডো এলিমেন্ট থেকে unfurl করে।
turbo:submit ইভেন্টে হুক করে সিঙ্ক প্রিভেন্ট করার চেষ্টা করলাম (টেবিল ব্লার করার সময়), কিন্তু সমাধানগুলো খুব hairy এবং ইন্টারনাল API-তে অতিরিক্ত নির্ভরশীল।
সবচেয়ে Turbo-esque সমাধান: পুরো ফর্মকে একটি eager-loaded Turbo Frame-এ wrap করুন এবং বলুন Trix কন্টেন্ট চেঞ্জ হলে রিলোড করতে।
<turbo-frame id="article_form" src="<%= edit_article_path(@article) %>"></turbo-frame>
এই পদ্ধতি শুধু already persisted base records-এ কাজ করে।
Trix-এ চূড়ান্ত সতর্কতা
আমাদের প্রুফ-অফ-কনসেপ্ট সার্ভার-রেন্ডার্ড HTML ব্যবহার করে টেবিলকে JSON-এ সিরিয়ালাইজ করার কমপ্লেক্সিটি এবং JS ইভেন্ট লিসেনিং এড়াতে। এটি যেকোনো ActionText ইনস্টলেশনে পোর্টেবল এবং সহজে gem-এ এক্সট্রাক্ট করা যায়।
কয়েকটি drawback আছে:
- Trix document model-র সাথে re-syncing: প্রস্তাবিত workaround কিছু পরিস্থিতিতে workable, অন্যগুলোতে no-go। Trix v2 Turbo-compatible interface পাওয়া পর্যন্ত এর কোথাও পথ নেই।
- Trix
undofunctionality ব্যবহার করে না: (এটি যেকোনো Trix attachment-এর জন্য true)। upstream changes এর জন্য অপেক্ষা করা বুদ্ধিমানী।
Wrap Up
এই পোস্টে আমরা ActionText Attachments-এর বেসিক্স দেখলাম, ActionText মডেলে টেবিল যোগ করলাম, Turbo Frames দিয়ে টুইক করলাম, এবং Trix-এর কিছু লিমিটেশন চর্চা করলাম।
Trix v2 underway (CoffeeScript থেকে modern JavaScript-এ ট্রান্সলেশন), এখন Turbo compatibility অ্যাড্রেস করার ভালো সময়। সেই wrapper কীভাবে দেখাবে সেটা আমার ক্ষমতার বাইরে, কিন্তু এটি একটি window of opportunity দেখাচ্ছে।
Happy coding!
P.S. Ruby Magic পোস্টগুলি প্রকাশ होते সাথে সাথে পেতে আমাদের Ruby Magic নিউজলেটার সাবস্ক্রাইব করুন!