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