ActionText in Ruby on Rails. Overview of the first meeting

Content

Target
Generated files
How our model instance is associated with the ActionText instance in the action_text_rich_texts table
How files/drawings are saved in posts
Total

Target

The ability to create, store and edit text with pictures, attached files is a common task. The essence of the task is to make a convenient tool for the content manager to add text data to the site.
IN
Ruby on Rails there is a module for this purpose actiontext With relevant documentation, which adds the ability to edit text in Rails. It includes an editor Trix. The editor is written in JavaScript, runs on the client (browser) side and draws a fairly friendly interface.

Trix editor

Trix editor

Creating the Model Used Below in the Examples Noteits controller, views (show/edit/new.html.erb), routing, and other basic actions related to this model are beyond the scope of this article.

Generated files

To get started with the module Action Text install the required libraries with the command:

bin/rails action_text:install

After executing this command, you will see a lot of modified and generated files:

List of generated files

List of generated files

It would be nice to understand what’s going on. Read the files and find out:

1. Gemfile, Gemfile.lock

In the previous step, we installed the gem image_processing. This package is needed to process (compress, resize) and upload images to the added content. This gem uses either ImageMagick/GraphicsMagickor libvips.

IN Gemfile.lock you can see the installed dependency libraries for image_processing. Their need is described Here.

For this gem to work, I added to config/application.rb line (from the description of the ActiveStorage::Variant class:

config.active_storage.variant_processor = :mini_magick

2.app/javascript/application.js

Imported JavaScript libraries here trix And @rails/actiontext.

3.config/importmap.rb

In this file, libraries and generated js-files are matched and given to the client in accordance with these names. Added JavaScript code for Trix. IN DevTools tab Debugger you can see these matches. An example in the figure below, where

1 – imports,

2 – js – files,

3 – Trix editor in the browser.

Let me remind you that a command is used to generate compiled css / js files (see. The Asset Pipeline:

bin/rails assets:precompile

4. test/fixtures/action_text/rich_texts.yml – file with test data

5. app/assets/stylesheets/actiontext.css – default styles

File contents
.trix-content .attachment-gallery > action-text-attachment,
.trix-content .attachment-gallery > .attachment {
  flex: 1 0 33%;
  padding: 0 0.5em;
  max-width: 33%;
}

.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment,
.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment,
.trix-content .attachment-gallery.attachment-gallery--4 > .attachment {
  flex-basis: 50%;
  max-width: 50%;
}

.trix-content action-text-attachment .attachment {
  padding: 0 !important;
  max-width: 100% !important;
}

It seems to be a regular css file, but we see that the element is used when generating html action-text-attachmentwhich is not in the list html elementsbut the html element is generated <action-text-attachment> With CustomElementRegistry.

6.db/migrate/20230210142600_create_active_storage_tables.active_storage.rb

This file describes a migration class that creates tables prefixed active_storage_:

* active_storage_blobs

* active_storage_attachments

* active_storage_variant_records

These tables are described in more detail in the documentation. Active Storage Overview.

File contents 20230210142600_create_active_storage_tables.active_storage.rb
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
  def change
    # Use Active Record's configured type for primary and foreign keys
    primary_key_type, foreign_key_type = primary_and_foreign_key_types

    create_table :active_storage_blobs, id: primary_key_type do |t|
      t.string   :key,          null: false
      t.string   :filename,     null: false
      t.string   :content_type
      t.text     :metadata
      t.string   :service_name, null: false
      t.bigint   :byte_size,    null: false
      t.string   :checksum

      if connection.supports_datetime_with_precision?
        t.datetime :created_at, precision: 6, null: false
      else
        t.datetime :created_at, null: false
      end

      t.index [ :key ], unique: true
    end

    create_table :active_storage_attachments, id: primary_key_type do |t|
      t.string     :name,     null: false
      t.references :record,   null: false, polymorphic: true, index: false, type: foreign_key_type
      t.references :blob,     null: false, type: foreign_key_type

      if connection.supports_datetime_with_precision?
        t.datetime :created_at, precision: 6, null: false
      else
        t.datetime :created_at, null: false
      end

      t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end

    create_table :active_storage_variant_records, id: primary_key_type do |t|
      t.belongs_to :blob, null: false, index: false, type: foreign_key_type
      t.string :variation_digest, null: false

      t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end
  end

  private
    def primary_and_foreign_key_types
      config = Rails.configuration.generators
      setting = config.options[config.orm][:primary_key_type]
      primary_key_type = setting || :primary_key
      foreign_key_type = setting || :bigint
      [primary_key_type, foreign_key_type]
    end
end

Here are written what fields will be created in the above tables, field restrictions on the uniqueness of primary and foreign keys. For example, in the table active_storage_attachments fields are created:

  • name with the restriction that it must not be empty,

  • record_type,

  • record_id with type bigintwhich with record_type are created with the option polymorphic: true (cm. Active Record Migrations References),

  • blob_id with type bigintThe referring to the entry id from the table active_storage_blobswhich has a foreign key constraint on blob_id,

  • a restriction is created on the uniqueness of the combination of fields record_type, record_id, name, blob_id.

7.db/migrate/20230210142601_create_action_text_tables.action_text.rb

This file describes a migration class that creates tables prefixed action_text_ (there is only one table):

In this table in the field body and the content will be stored as html. All links to images and files will be generated during the creation of this content and immediately embedded in the text.

File contents 20230210142601_create_action_text_tables.action_text.rb
# This migration comes from action_text (originally 20180528164100)
class CreateActionTextTables < ActiveRecord::Migration[6.0]
  def change
    # Use Active Record's configured type for primary and foreign keys
    primary_key_type, foreign_key_type = primary_and_foreign_key_types

    create_table :action_text_rich_texts, id: primary_key_type do |t|
      t.string     :name, null: false
      t.text       :body, size: :long
      t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type

      t.timestamps

      t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
    end
  end

  private
    def primary_and_foreign_key_types
      config = Rails.configuration.generators
      setting = config.options[config.orm][:primary_key_type]
      primary_key_type = setting || :primary_key
      foreign_key_type = setting || :bigint
      [primary_key_type, foreign_key_type]
    end
end

After running the migration with the command bin/rails db:migrate as a result, tables should appear in your database.

8.app/views/layouts/action_text/contents/_content.html.erb

The content of the _content.html.erb file
<div class="trix-content">
  <%= yield %>
</div>

Inserted here bodywhich is saved as html, from a table action_text_rich_texts.

9.app/views/active_storage/blobs/_blob.html.erb

The contents of the _blob.html.erb file
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
  <% if blob.representable? %>
    <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
  <% end %>

  <figcaption class="attachment__caption">
    <% if caption = blob.try(:caption) %>
      <%= caption %>
    <% else %>
      <span class="attachment__name"><%= blob.filename %></span>
      <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
    <% end %>
  </figcaption>
</figure>

<!--Если хочется иметь возможность скачать файл, можно привести ссылку на файл.-->
<% if !blob.image? %>
  <div>
    <span class="attachment__download"><%= link_to blob.filename, rails_blob_path(blob) %></span>
    <span class="attachment__size">(<%= number_to_human_size blob.byte_size %>)</span>
  </div>
<% end %>

In DevTools, saved content with a picture looks like this:

In this html piece of the page, a preview of the file and an image are drawn, the data about which is stored in the table active_storage_blobs.

The description for the picture loaded through the Trix editor is stored as part of the text in the table action_text_rich_texts.

IN _blob.html.erb I added the ability to download a file, as opposed to the default file.

How does our model instance communicate with the ActionText instance in the action_text_rich_texts table?

In the model, I write the association has_rich_text.

class Note < ApplicationRecord
  has_rich_text :body
end

Then, when creating an instance in the controller, I add to the allowed parameters body:

class NotesController < ApplicationController
  def create
    @note = Note.create!(note_params)

    redirect_to note_path @note
  end
  
  private

  def note_params = params.require(:note).permit(:title, :body)
end

Through has_rich_text the corresponding method from module ActionText::Attribute.

Code for this method from the repository rails I read like this:

  • Methods are created for the model instance: predicate body?setter and getter for this field.

  • Created association rich_text_body with class name ActionText::RichTextwith field name name. as: :record – sign polymorphic connectionwhich was also seen in the migration when creating the table action_text_rich_texts. This association is inverse, i.e. from a model instance ActionText::RichText you can get the value of this field through a method call record.

I check the above in the console:
> note = Note.find(1)
=begin
<Note:0x00007efee9a06ce0                                                                
 id: 1,                                                                                  
 title: "РыбаТекст помогает животным",                                                
 hidden: false,                                                                          
 created_at: Sun, 19 Feb 2023 11:24:47.510481000 UTC +00:00,                             
 updated_at: Sun, 19 Feb 2023 11:24:47.613920000 UTC +00:00> 
=end
>
> note.body
=begin
<ActionText::RichText:0x00007efee9848688
 id: 1,                                 
 name: "body",                       
 body: #<ActionText::Content "<div class=\"trix-conte...">,
 record_type: "Note",                   
 record_id: 1,                          
 created_at: Sun, 19 Feb 2023 11:24:47.564128000 UTC +00:00,
 updated_at: Sun, 19 Feb 2023 11:24:47.583755000 UTC +00:00>
=end
>
> action_text_item = ActionText::RichText.find(1)
=begin
<ActionText::RichText:0x00007efee9848688
 id: 1,                                 
 name: "body",                       
 body: #<ActionText::Content "<div class=\"trix-conte...">,
 record_type: "Note",                   
 record_id: 1,                          
 created_at: Sun, 19 Feb 2023 11:24:47.564128000 UTC +00:00,
 updated_at: Sun, 19 Feb 2023 11:24:47.583755000 UTC +00:00>
=end
>
> action_text_item.record
=begin
<Note:0x00007efee9a06ce0                                                                
 id: 1,                                                                                  
 title: "РыбаТекст помогает животным",                                                
 hidden: false,                                                                          
 created_at: Sun, 19 Feb 2023 11:24:47.510481000 UTC +00:00,                             
 updated_at: Sun, 19 Feb 2023 11:24:47.613920000 UTC +00:00>
=end

Altogether, it is clear that note.body and find an instance ActionText::RichText according to the well-known id refer to the same entry, which indicates that the option inverse_of works as expected.

How are files/drawings saved in posts?

When creating a new entry in the Trix editor, by clicking on the paperclip icon you can add a file. When you select one, for example, a picture, in the console where the server is running, you can see that a request is being made POST "/rails/active_storage/direct_uploads"which is being processed ActiveStorage::DirectUploadsController#create with parameters:

{
  "blob": {
    "filename": "cat.jpg",
    "content_type": "image/jpeg",
    "byte_size": 107329,
    "checksum": "JuFhMdR3g4JjS73owAJLQA=="
  },
  "direct_upload": {
    "blob": {
      "filename": "cat.jpg",
      "content_type": "image/jpeg",
      "byte_size": 107329,
      "checksum": "JuFhMdR3g4JjS73owAJLQA=="
      }
    }
  }

IN ActiveStorage::DirectUploadsController#create going on preservation desired parameters of this file.

In the console it looks like this:

TRANSACTION (0.4ms)  BEGIN
ActiveStorage::Blob Create (0.7ms)  INSERT INTO "active_storage_blobs" ("key", "filename", "content_type", "metadata", "service_name", "byte_size", "checksum", "created_at") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id"  [["key", "ikydgdsgyrgupbqqh0wdqtgs75y4"], ["filename", "cat.jpg"], ["content_type", "image/jpeg"], ["metadata", nil], ["service_name", "minio"], ["byte_size", 107329], ["checksum", "JuFhMdR3g4JjS73owAJLQA=="], ["created_at", "2023-02-23 16:35:59.956614"]]
TRANSACTION (0.8ms)  COMMIT
S3 Storage (45.7ms) Generated URL for file at key: ikydgdsgyrgupbqqh0wdqtgs75y4 (http://localhost:9000/main/ikydgdsgyrgupbqqh0wdqtgs75y4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20230223%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20230223T163600Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-length%3Bcontent-md5%3Bcontent-type%3Bhost&X-Amz-Signature=696eaa54d2f921beaa552de1263fdb8b548708d12ebbdd3df6e97d7077dedeff)

By default, files are stored on the local drive, described as local in file config/storage.yml and configured by default. In this case service will Disk. Of value key saved file, the file name and its folder are generated and all folders with these files are stored in the folder storage.

When the content manager adds text, clicks on the Publish button and saves the entry. The link to the file is stored directly in the body rich_text. An entry is made in the table active_storage_attachmentswhich maintains relationships between tables action_text_rich_texts And active_storage_blobs.

Table active_storage_attachments:

  • in field record_id preserved id records from the table action_text_rich_texts,

  • in field record_type the name of the module/class of the record from the table is saved action_text_rich_texts,

  • in field blob_id preserved id file/picture from table active_storage_blobs,

  • in field name the file/picture type is saved.

Total

Covered:

I’ll finish with this. Obviously, that’s not all. Next you need to do:

Enjoy programming, friends!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *