Handling attachments in Action Text in Rails 6


This is part two of our multipart series exploring Action Text, In part 1, we looked at basic of how we can get started with providing WYSIWYG support in our Apps using ActionText.

This blog post will deal with file attachments in Action Text.

Quick setup

We have discussed setup steps in Part 1. Here we will just list a set of commands to setup a new rails app and enable Action Text which one can execute so as to follow along this post.

Create a new rails app:

$ rails new blog_app --database=postgresql

Switch to app directory:

$ cd blog_app

Create database:

$ rails db:create

Enable support for action_text:

$ rails action_text:install
$ rails db:migrate

Add scaffold for post resource:

$ rails g scaffold Post
$ rails db:migrate

Update models and controller files for post:

# app/models/post.rb
class Post < ApplicationRecord
  has_rich_text :content
end

# app/controllers/posts_controller.rb
# Just update the post_params function to permit `:content`
...
def post_params
  params.fetch(:post, {}).permit(:content)
end
...

Update the post form to edit content:

<!--app/views/posts/_form.html.erb-->
<%= form_with(model: post, local: true) do |form| %>
  <div class="field">
    <%= form.label :content %>
    <%= form.rich_text_area :content %>
  </div>
  <% if post.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
        <% post.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

Update post show view to render content:

<!--app/views/messages/show.html.erb-->
<%= @message.content %>
<p id="notice"><%= notice %></p>

<%= link_to 'Edit', edit_message_path(@message) %> |
<%= link_to 'Back', messages_path %>
$ rails s

We can test the setup by opening the following URL in our browser.

http://localhost:3000/posts

Add/Uncomment image processing gem. We can skip this if we don’t want to resize images when displaying the rich text.

gem 'image_processing', '~> 1.2'

We can now perform CRUD on post.

Click on New post or go to http://localhost:3000/posts/new. We should see the Trix editor rendered.

Now let’s create a file app/javascript/trix-editor-overrides.js and import it in app/javascript/packs/application.js as shown below.

// app/javascript/packs/application.js
...
import "../trix-editor-overrides"
...

Control which file can be attached in Trix editor

The default setup of Action Text allows attaching any kind of file. We can test it out. Try attaching a zip file to a new post. Due to business, performance or security reason, this may not be desirable behaviour. Let’s see how can we gain more control.

Trix event trix-file-accept

Trix fire events for every action that can be performed in trix. One of the event is trix-file-accept. This event is fired before a file gets attached in Trix. If we call preventDefault() on this event then the file attachment/upload would not happen and Trix would ignore the file.

Blacklist all file attachments:

Change the trix-editor-overrides.js as shown below.

// app/javascript/trix-editor-overrides.js
window.addEventListener("trix-file-accept", function(event) {
  event.preventDefault()
  alert("File attachment not supported!")
})

Reload the http://localhost:3000/posts/new page and try attaching any file we should get an alert and the file wont get attached.

Failed to attach any file. Failed to attach any file.

We can use a File object’s properties in combination with trix-file-accept event to fine tune which files we want to upload and which we want to reject. Let’s see few examples.

Only whitelist certain files based type:

Change the trix-editor-overrides.js as shown below. This will only allow upload of jpeg and png file types. Every other file type will be discarded with an alert.

// app/javascript/trix-editor-overrides.jslotr

window.addEventListener("trix-file-accept", function(event) {
  const acceptedTypes = ['image/jpeg', 'image/png']
  if (!acceptedTypes.includes(event.file.type)) {
    event.preventDefault()
    alert("Only support attachment of jpeg or png files")
  }
})
Failed to attach files which are not jped or png. Failed to attach files which are not jped or png.

Only whitelist certain files based size:

Change the trix-editor-overrides.js as shown below.
This will prevent upload of any file which is more then 1MB in size.

// app/javascript/trix-editor-overrides.js
window.addEventListener("trix-file-accept", function(event) {
  const maxFileSize = 1024 * 1024 // 1MB 
  if (event.file.size > maxFileSize) {
    event.preventDefault()
    alert("Only support attachment files upto size 1MB!")
  }
})

If we reload and try attaching a file > 1MB, we will get alert but any file under 1MB will get attached properly.

Failed to attach file with size > 1MB Failed to attach file with size > 1MB

Understanding _blob.html.erb

Located under app/views/active_storage/blobs/, this partial is auto created by Rails when were run the rails action_text:install script.

This gets called for every attachment that is part of the Action Text content we try to render and is responsible for how a attachment is rendered.

In our blog app create a new post with 2 attachments and save the post.

Show page with 2 image attachments using the default _blob.html.erb Show page with 2 image attachments using the default _blob.html.erb

Now, lets replace all the content of app/views/active_storage/blobs/_blob.html.erb with the following

  Hello world.

Reload the show page of the latest post.

Show page of the latest post. Every instance of attachment is replaced with their hello world Show page of the latest post. Every instance of attachment is replaced with their hello world

A variable named blob which is a ActiveStorage::Blob object is always passed to this partial. This blob object is how we can access all the properties of the attached file.

For Example; if we replace the content of app/views/active_storage/blobs/_blob.html.erb with the following

  <%= blob.filename %>

Reload the show page of the latest post.

Show page of the latest post. Every instance of attachment is replaced with their file name. Show page of the latest post. Every instance of attachment is replaced with their file name.

Now let’s try and understand the default _blob.html.erb that Rails generates.

  <!--app/views/active_storage/blobs/_blob.html.erb-->
  <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>

Let’s break this code down to understand it further.

  <% if blob.representable? %>
    <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
  <% end %>

The above line checks if attachment blob is of type image or can be converted and previewed as image. If yes, then it will fetch the image representation of specific size and display it. By default the statement blob.representable? will return true for images and false for any other media type.

  <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>

This block is pretty straight forward. If we had specified caption for attachment during upload then it will display caption along with the attachment, else it will display the attachment filename and byte size.

Let’s see this code in action by trying to PDFs and Videos

Image preview for PDF.

According to Rails guide,

Extracting previews requires third-party applications, FFmpeg for video and muPDF for PDFs, and on macOS also XQuartz and Poppler. These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you install and use the third-party software, make sure you understand the licensing implications of doing so.

Let create a new post with PDF attachment without installing third-party support software.

Creating new post with PDF attachment Creating new post with PDF attachment

Below is how Active Text content is rendered.

Viewing the newly created post. We cannot see any preview of the attached PDF as the 3rd party library needed to generate the preview is missing. Viewing the newly created post. We cannot see any preview of the attached PDF as the 3rd party library needed to generate the preview is missing.

Now let’s install the software. For mac that would be Poppler. We are going to use Homebrew for that.

  brew install poppler

Restart the rails server after the 3rd part library is installed.

Let’s reload the show page for the above post. Now we should see a preview of the attachment.

Reloading the  after installing poppler. We can now see the preview of the 1st page of the attached PDF. Reloading the after installing poppler. We can now see the preview of the 1st page of the attached PDF.

We can add support for previewing all kinds of attachment with the help of Active Storage Previewers.
Read more about them here