কম্পিউটার টিউটোরিয়াল

Trix এবং Turbo Frames ব্যবহার করে Rails-এ ডাইনামিক টেবিল এডিটর তৈরি করুন

এই পোস্টে আমরা আপনার 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-এর সাথে কনফর্ম করতে একটি ক্লাসকে শুধু দুটি কাজ করতে হয়:

  1. to_sgid ইমপ্লিমেন্ট করুন GlobalID::Identification ইনক্লুড করে। ডিফল্টলি সব ActiveRecord::Base descendents এটি করে।
  2. 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" গ্রুপে সফলি অ্যাড হয়েছে:

Trix এবং Turbo Frames ব্যবহার করে Rails-এ ডাইনামিক টেবিল এডিটর তৈরি করুন

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 যোগ করলেই ফলাফল। একক বাটন ক্লিকে আর্টিকেল কন্টেন্টে টেবিল যোগ করা যায়:

Trix এবং Turbo Frames ব্যবহার করে Rails-এ ডাইনামিক টেবিল এডিটর তৈরি করুন

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

বাটন ক্লিক দিয়ে টেবিলের স্ট্রাকচার চেঞ্জ করা যায়:

Trix এবং Turbo Frames ব্যবহার করে Rails-এ ডাইনামিক টেবিল এডিটর তৈরি করুন

টেবিল সেল কন্টেন্ট এডিট করুন

রো/কলাম সংখ্যা চেঞ্জ করার পাশাপাশি, সেল কন্টেন্টও এডিট করতে চাই। 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 এবং Turbo Frames ব্যবহার করে Rails-এ ডাইনামিক টেবিল এডিটর তৈরি করুন

পেজ রিফ্রেশ করলে অ্যাডেড কন্টেন্ট দেখাবে, কারণ 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 আছে:

  1. Trix document model-র সাথে re-syncing: প্রস্তাবিত workaround কিছু পরিস্থিতিতে workable, অন্যগুলোতে no-go। Trix v2 Turbo-compatible interface পাওয়া পর্যন্ত এর কোথাও পথ নেই।
  2. Trix undo functionality ব্যবহার করে না: (এটি যেকোনো 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 নিউজলেটার সাবস্ক্রাইব করুন!

  1. Matplotlib-এ মাউস ক্লিক দিয়ে (x, y) কোঅর্ডিনেট পাওয়ার পদ্ধতি

  2. জাভাস্ক্রিপ্টে ইম্পোর্ট করা অবজেক্ট ডিস্ট্রাকচার করা

  3. HTML required অ্যাট্রিবিউট

  4. MS SQL Server-এ ইউজার পারমিশন ডিসেন্ট্রালাইজ করার প্র্যাক্টিক্যাল গাইড