Custom Attachments in Action Text
Custom attachments allow you use your own models as attachments in Action Text documents. This, for example, can be helpful when implementing @mentions or if you want to highlight some of your products in a blog post.
You can find the code for this post at https://github.com/Dennitz/ActionTextAttachments
What we will build
We will build a simple project with a Post
and a Product
model. The Post
model has an Action Text document, in which we’d like to be able to insert a Product
.
More specifically, it should be possible to show the products/product
partial
inside the Action Text document. The result looks like this:
When any update is made on the attached model instance, it is automatically reflected in the Action Text document:
The Models
First, let’s create the Post
model with its controller, views, etc.
rails generate scaffold Post
Then add a rich text field named content
.
1
2
3
class Post < ApplicationRecord
has_rich_text :content
end
app/models/post.rb
Secondly, create the Product
scaffold:
rails generate scaffold Product name:string 'price:decimal{8,2}'
To be able to use a model for custom attachments, the model must have the attachable_sgid
method. So let’s make it available on the Product
model by including
ActionText::Attachable
:
1
2
3
4
5
class Product < ApplicationRecord
include ActionText::Attachable
has_one_attached :image
end
app/models/product.rb
The attachable_sgid
method is needed because Action Text uses Signed Global IDs (sgid) to identify attachments and their models.
By using the sgid, Action Text can re-render the model’s partial every time, instead of just storing its HTML. Because of this, updates to an attached model
are reflected in the Action Text document.
The Form
The Post
form calls form.rich_text_area
to create the Action Text input. Below that it
has a Bootstrap dropdown, which includes a button for each product name. Each of
those buttons has the product’s id added to the button’s dataset so that the id
can be used in JavaScript code.
The form is connected to the attachments
Stimulus controller, which is used to insert
the custom attachment whenever one of the buttons in the dropdown is clicked.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<%= form_with(model: post, local: true,
data: { controller: "attachments"} ) do |form| %>
<div class="field">
<%= form.label :content %>
<%= form.rich_text_area :content, data: { target: "attachments.editor" } %>
</div>
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
Add model attachment
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<% @products.each do |product| %>
<%= tag.button product.name,
class: "dropdown-item",
type: "button",
data: { action: "attachments#attach", product_id: product.id } %>
<% end %>
</div>
</div>
<div class="actions mt-4">
<%= form.submit %>
</div>
<% end %>
app/views/posts/_form.html.erb
In a real project, instead of using a dropdown to show all product names, you can, for example, do an autocomplete search through all the models you might want to include in an Action Text document.
The Stimulus Controller
Whenever a button in the dropdown is clicked, the attach
method
of the following Stimulus controller is called.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Controller } from 'stimulus';
import Trix from 'trix';
export default class extends Controller {
static targets = ['editor'];
attach(event) {
const { productId } = event.target.dataset;
fetch(`/products/${productId}.json`)
.then(response => response.json())
.then(product => this._createAttachment(product))
.catch(error => {
console.log('error', error);
});
}
_createAttachment(product) {
const editor = this.editorTarget.editor;
const attachment = new Trix.Attachment({
sgid: product.sgid,
content: product.content,
});
editor.insertAttachment(attachment);
editor.insertString(' ');
}
}
app/javascript/controllers/attachments_controller.js
The attach
method gets the product id from the dataset of the clicked button. It
then does a fetch using this id to receive the sgid and the rendered content for that product.
The fetch triggers the show
action of the ProductsController
to be called.
As the json format is requested, this will render the following file:
1
json.partial! "products/product", product: @product
app/views/products/show.json.jbuilder
This just renders the following jbuilder partial:
1
2
3
4
5
6
json.sgid product.attachable_sgid
json.content render(
partial: 'products/product',
locals: { product: product },
formats: %i[html]
)
app/views/products/_product.json.jbuilder
This will include the sgid
of the product and store the rendered HTML partial of
the product under the content
key.
Next, a Trix.Attachment
is created in the Stimulus controller using the fetched
sgid and content. This attachment is then inserted into the editor, which will
then show the HTML that was stored under the content
key.
Note that this HTML partial is only shown when first inserted as an attachment.
On subsequent renders, Action Text will use the sgid to
figure out that it is working with a Product
and thus has to render the
products/product
partial. This ensures that it will always show
up-to-date data and allows updates to a product to be reflected in the Action Text
document automatically.
Styling
Custom CSS may be needed for elements inside an Action Text document. The content
of an Action Text document is wrapped with the .trix-content
class, so other CSS
can be scoped under this class, e.g.
1
2
3
4
5
6
.trix-content {
.card {
max-width: 14rem;
display: inline-block;
}
}
app/assets/stylesheets/actiontext.scss
Grouping Attachments
When images are placed side by side in an Action Text document, the image attachments
are grouped with a surrounding div that has the .attachment-gallery
class. By
default, custom attachments are not grouped in a surrounding div.
If you want them to be grouped, you have to adjust Trix’s config by calling:
Trix.config.attachments.content = { presentation: 'gallery' }
This can, for example, be done in the connect
method of the Stimulus controller.
Discussion and feedback