diff --git a/actiontext/CHANGELOG.md b/actiontext/CHANGELOG.md
index 916e4617f7..5853a0e4d2 100644
--- a/actiontext/CHANGELOG.md
+++ b/actiontext/CHANGELOG.md
@@ -1,3 +1,11 @@
+* Expose how we render the HTML _surrounding_ rich text content as an
+ extensible `layouts/action_view/contents/_content.html.erb` template to
+ encourage user-land customizations, while retaining private API control over how
+ the rich text itself is rendered by `action_text/contents/_content.html.erb`
+ partial.
+
+ *Sean Doyle*
+
* Add `with_all_rich_text` method to eager load all rich text associations on a model at once.
*Matt Swanson*, *DHH*
diff --git a/actiontext/app/helpers/action_text/content_helper.rb b/actiontext/app/helpers/action_text/content_helper.rb
index 0d0bb19de2..d7c32a470d 100644
--- a/actiontext/app/helpers/action_text/content_helper.rb
+++ b/actiontext/app/helpers/action_text/content_helper.rb
@@ -22,17 +22,31 @@ module ActionText
content.render_attachments do |attachment|
unless attachment.in?(content.gallery_attachments)
attachment.node.tap do |node|
- node.inner_html = render(attachment, in_gallery: false).chomp
+ node.inner_html = render_action_text_attachment attachment, locals: { in_gallery: false }
end
end
end.render_attachment_galleries do |attachment_gallery|
render(layout: attachment_gallery, object: attachment_gallery) do
attachment_gallery.attachments.map do |attachment|
- attachment.node.inner_html = render(attachment, in_gallery: true).chomp
+ attachment.node.inner_html = render_action_text_attachment attachment, locals: { in_gallery: true }
attachment.to_html
end.join.html_safe
end.chomp
end
end
+
+ def render_action_text_attachment(attachment, locals: {}) #:nodoc:
+ options = { locals: locals, object: attachment, partial: attachment }
+
+ if attachment.respond_to?(:to_attachable_partial_path)
+ options[:partial] = attachment.to_attachable_partial_path
+ end
+
+ if attachment.respond_to?(:model_name)
+ options[:as] = attachment.model_name.element
+ end
+
+ render(**options).chomp
+ end
end
end
diff --git a/actiontext/app/views/action_text/content/_layout.html.erb b/actiontext/app/views/action_text/content/_layout.html.erb
deleted file mode 100644
index 55cb708ac4..0000000000
--- a/actiontext/app/views/action_text/content/_layout.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-
- <%= render_action_text_content(content) %>
-
diff --git a/actiontext/app/views/action_text/contents/_content.html.erb b/actiontext/app/views/action_text/contents/_content.html.erb
new file mode 100644
index 0000000000..898a506677
--- /dev/null
+++ b/actiontext/app/views/action_text/contents/_content.html.erb
@@ -0,0 +1 @@
+<%= render_action_text_content(content) %>
diff --git a/actiontext/app/views/layouts/action_text/contents/_content.html.erb b/actiontext/app/views/layouts/action_text/contents/_content.html.erb
new file mode 100644
index 0000000000..9e3c0d0dff
--- /dev/null
+++ b/actiontext/app/views/layouts/action_text/contents/_content.html.erb
@@ -0,0 +1,3 @@
+
+ <%= yield -%>
+
diff --git a/actiontext/lib/action_text/attachable.rb b/actiontext/lib/action_text/attachable.rb
index 3343bcc308..f818fb58d7 100644
--- a/actiontext/lib/action_text/attachable.rb
+++ b/actiontext/lib/action_text/attachable.rb
@@ -71,6 +71,10 @@ module ActionText
to_partial_path
end
+ def to_attachable_partial_path
+ to_partial_path
+ end
+
def to_rich_text_attributes(attributes = {})
attributes.dup.tap do |attrs|
attrs[:sgid] = attachable_sgid
diff --git a/actiontext/lib/action_text/content.rb b/actiontext/lib/action_text/content.rb
index ab8fc05c8c..de43880a0c 100644
--- a/actiontext/lib/action_text/content.rb
+++ b/actiontext/lib/action_text/content.rb
@@ -84,7 +84,11 @@ module ActionText
end
def to_rendered_html_with_layout
- render partial: "action_text/content/layout", formats: :html, locals: { content: self }
+ render layout: "action_text/contents/content", partial: to_partial_path, formats: :html, locals: { content: self }
+ end
+
+ def to_partial_path
+ "action_text/contents/content"
end
def to_s
diff --git a/actiontext/lib/generators/action_text/install/install_generator.rb b/actiontext/lib/generators/action_text/install/install_generator.rb
index 4255b614a6..b583826c1a 100644
--- a/actiontext/lib/generators/action_text/install/install_generator.rb
+++ b/actiontext/lib/generators/action_text/install/install_generator.rb
@@ -47,6 +47,9 @@ module ActionText
copy_file "#{GEM_ROOT}/app/views/active_storage/blobs/_blob.html.erb",
"app/views/active_storage/blobs/_blob.html.erb"
+
+ copy_file "#{GEM_ROOT}/app/views/layouts/action_text/contents/_content.html.erb",
+ "app/views/layouts/action_text/contents/_content.html.erb"
end
def create_migrations
diff --git a/actiontext/test/dummy/app/models/person.rb b/actiontext/test/dummy/app/models/person.rb
index 0ded356d5b..2e7ea573e1 100644
--- a/actiontext/test/dummy/app/models/person.rb
+++ b/actiontext/test/dummy/app/models/person.rb
@@ -4,4 +4,8 @@ class Person < ApplicationRecord
def to_trix_content_attachment_partial_path
"people/trix_content_attachment"
end
+
+ def to_attachable_partial_path
+ "people/attachable"
+ end
end
diff --git a/actiontext/test/dummy/app/views/people/_attachable.html.erb b/actiontext/test/dummy/app/views/people/_attachable.html.erb
new file mode 100644
index 0000000000..b423eb4981
--- /dev/null
+++ b/actiontext/test/dummy/app/views/people/_attachable.html.erb
@@ -0,0 +1 @@
+<%= person.name %>
diff --git a/actiontext/test/fixtures/action_text/rich_texts.yml b/actiontext/test/fixtures/action_text/rich_texts.yml
index 8c1d7a1c2d..c1d917a1bb 100644
--- a/actiontext/test/fixtures/action_text/rich_texts.yml
+++ b/actiontext/test/fixtures/action_text/rich_texts.yml
@@ -1,3 +1,8 @@
+hello_alice_message_content:
+ record: hello_alice (Message)
+ name: content
+ body: Hello, <%= ActionText::FixtureSet.attachment("people", :alice) %>
+
hello_world_review_content:
record: hello_world (Review)
name: content
diff --git a/actiontext/test/fixtures/messages.yml b/actiontext/test/fixtures/messages.yml
index 696fb8a163..bb5171f6d4 100644
--- a/actiontext/test/fixtures/messages.yml
+++ b/actiontext/test/fixtures/messages.yml
@@ -1,2 +1,5 @@
+hello_alice:
+ subject: "A message to Alice"
+
hello_world:
subject: "A greeting"
diff --git a/actiontext/test/fixtures/people.yml b/actiontext/test/fixtures/people.yml
new file mode 100644
index 0000000000..2ceb0704aa
--- /dev/null
+++ b/actiontext/test/fixtures/people.yml
@@ -0,0 +1,2 @@
+alice:
+ name: "Alice"
diff --git a/actiontext/test/integration/controller_render_test.rb b/actiontext/test/integration/controller_render_test.rb
index b731af4cb3..cf9667401e 100644
--- a/actiontext/test/integration/controller_render_test.rb
+++ b/actiontext/test/integration/controller_render_test.rb
@@ -31,4 +31,12 @@ class ActionText::ControllerRenderTest < ActionDispatch::IntegrationTest
get admin_message_path(message)
assert_select "#content-html .trix-content .attachment--jpg"
end
+
+ test "resolves ActionText::Attachable based on their to_attachable_partial_path" do
+ alice = people(:alice)
+
+ get messages_path
+
+ assert_select ".mentioned-person", text: alice.name
+ end
end
diff --git a/guides/source/action_text_overview.md b/guides/source/action_text_overview.md
index 3a2177635e..013c842c4e 100644
--- a/guides/source/action_text_overview.md
+++ b/guides/source/action_text_overview.md
@@ -10,7 +10,7 @@ After reading this guide, you will know:
* How to configure Action Text.
* How to handle rich text content.
-* How to style rich text content.
+* How to style rich text content and attachments.
--------------------------------------------------------------------------------
@@ -70,9 +70,9 @@ After the installation is complete, a Rails app using Webpacker should have the
@import "./actiontext.scss";
```
-## Examples
+## Creating Rich Text content
-Adding a rich text field to an existing model:
+Add a rich text field to an existing model:
```ruby
# app/models/message.rb
@@ -81,7 +81,7 @@ class Message < ApplicationRecord
end
```
-Note that you don't need to add a `content` field to your `messages` table.
+**Note:** you don't need to add a `content` field to your `messages` table.
Then refer to this field in the form for the model:
@@ -112,6 +112,112 @@ class MessagesController < ApplicationController
end
```
+## Rendering Rich Text content
+
+Action Text will sanitize and render rich content on your behalf.
+
+By default, the Action Text editor and content is styled by the Trix defaults.
+
+If you want to change these defaults, remove the `// require "actiontext.scss"`
+line from your `application.scss` to omit the [contents of that
+file](https://raw.githubusercontent.com/basecamp/trix/master/dist/trix.css).
+
+By default, Action Text will render rich text content into an element that
+declares the `.trix-content` class:
+
+```html+erb
+<%# app/views/layouts/action_text/contents/_content.html.erb %>
+
+ <%= yield %>
+
+```
+
+If you'd like to change the rich text's surrounding HTML with your own layout,
+declare your own `app/views/layouts/action_text/contents/_content.html.erb`
+template and call `yield` in place of the content.
+
+You can also style the HTML used for embedded images and other attachments
+(known as blobs). On installation, Action Text will copy over a partial to
+`app/views/active_storage/blobs/_blob.html.erb`, which you can specialize.
+
+### Rendering attachments
+
+In addition to attachments uploaded through Active Storage, Action Text can
+embed anything that can be resolved by a [Signed
+GlobalID](https://github.com/rails/globalid#signed-global-ids).
+
+Action Text renders embedded `` elements by resolving
+their `sgid` attribute into an instance. Once resolved, that instance is passed
+along to
+[`render`](https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/RenderingHelper.html#method-i-render).
+The resulting HTML is embedded as a descendant of the ``
+element.
+
+For example, consider a `User` model:
+
+```ruby
+# app/models/user.rb
+class User < ApplicationRecord
+ has_one_attached :avatar
+end
+
+user = User.find(1)
+user.to_global_id.to_s #=> gid://MyRailsApp/User/1
+user.to_signed_global_id.to_s #=> BAh7CEkiCG…
+```
+
+Next, consider some rich text content that embeds an ``
+element that references the `User` instance's signed GlobalID:
+
+```html
+Hello, .
+```
+
+Aciton Text resolves uses the "BAh7CEkiCG…" String to resolve the `User`
+instance. Next, consider the application's `users/user` partial:
+
+```html+erb
+<%# app/views/users/_user.html.erb %>
+<%= image_tag user.avatar %> <%= user.name %>
+```
+
+The resulting HTML rendered by Aciton Text would look something like:
+
+```html
+Hello, Jane Doe.
+```
+
+To render a different partial, define `User#to_attachable_partial_path`:
+
+```ruby
+class User < ApplicationRecord
+ def to_attachable_partial_path
+ "users/attachable"
+ end
+end
+```
+
+Then declare that partial. The `User` instance will be available as the `user`
+partial-local variable:
+
+```html+erb
+<%# app/views/users/_attachable.html.erb %>
+<%= image_tag user.avatar %> <%= user.name %>
+```
+
+To integrate with Aciton Text `` element rendering, a
+class must:
+
+* include the `ActionText::Attachable` module
+* implement `#to_sgid(**options)` (made available through the [`GlobalID::Identification` concern][global-id])
+* (optional) declare `#to_attachable_partial_path`
+
+By default, all `ActiveRecord::Base` descendants mix-in
+[`GlobalID::Identification` concern][global-id], and are therefore
+`ActionText::Attachable` compatible.
+
+[global-id]: https://github.com/rails/globalid#usage
+
## Avoid N+1 queries
If you wish to preload the dependent `ActionText::RichText` model, assuming your rich text field is named `content`, you can use the named scope:
@@ -121,17 +227,6 @@ Message.all.with_rich_text_content # Preload the body without attachments.
Message.all.with_rich_text_content_and_embeds # Preload both body and attachments.
```
-## Custom styling
-
-By default, the Action Text editor and content is styled by the Trix defaults.
-If you want to change these defaults, you'll want to remove
-the `app/assets/stylesheets/actiontext.scss` linker and base your stylings on
-the [contents of that file](https://raw.githubusercontent.com/basecamp/trix/master/dist/trix.css).
-
-You can also style the HTML used for embedded images and other attachments (known as blobs).
-On installation, Action Text will copy over a partial to
-`app/views/active_storage/blobs/_blob.html.erb`, which you can specialize.
-
## API / Backend development
1. A backend API (for example, using JSON) needs a separate endpoint for uploading files that creates an `ActiveStorage::Blob` and returns its `attachable_sgid`:
diff --git a/railties/test/generators/action_text_install_generator_test.rb b/railties/test/generators/action_text_install_generator_test.rb
index 2bd3ca2fb6..edf8a5b69e 100644
--- a/railties/test/generators/action_text_install_generator_test.rb
+++ b/railties/test/generators/action_text_install_generator_test.rb
@@ -60,6 +60,12 @@ class ActionText::Generators::InstallGeneratorTest < Rails::Generators::TestCase
assert_file "app/views/active_storage/blobs/_blob.html.erb"
end
+ test "creates Action Text content view layout" do
+ run_generator_instance
+
+ assert_file "app/views/layouts/action_text/contents/_content.html.erb"
+ end
+
test "creates migrations" do
run_generator_instance