Improve ActionText extensiblibility

Extensible layout
---

Expose how we render the HTML _surrounding_ rich text content as an
extensible `layouts/action_text/contents/_content.html.erb` template to
encourage user-land customizations, while retaining private API control
over how the rich text itself is rendered by moving the
`#render_action_text_content` helper invocation to the
`action_text/contents/_content.html.erb` partial.

Extensible Attachable `#to_attachable_partial_path`
---

When an application declares a canonical partial for a record, there is
no way to override which partial is used when transformed to Rich Text.
For example, a default `Person < ApplicationRecord` instance returns
`"people/person"` from calls to `#to_partial_path`, resulting in the
`app/views/people/_person.html.erb` partial being rendered.

Prior to this change, when encountering an `<action-text-attachment
sgid="...">` element, ActionText retrieved the corresponding
`Attachable` instance (usually an `ActiveRecord::Base` instance) and
transformed it to rich text HTML by rendering the partial that
corresponds to its `#to_partial_path`.

This proposed change instead invokes
`Attachable#to_attachable_partial_path`. By default,
`#to_attachable_partial_path` is an alias for `#to_partial_path`.

Guides
---

Extend the `guides/action_text_overview` document to
describe how to customize these templates, and to better illustrate how
ActionText::Attachable instances are rendered into HTML.
This commit is contained in:
Sean Doyle 2020-09-29 23:16:18 -04:00
parent cc2e097fed
commit 3500571b43
16 changed files with 179 additions and 21 deletions

View File

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

View File

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

View File

@ -1,3 +0,0 @@
<div class="trix-content">
<%= render_action_text_content(content) %>
</div>

View File

@ -0,0 +1 @@
<%= render_action_text_content(content) %>

View File

@ -0,0 +1,3 @@
<div class="trix-content">
<%= yield -%>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
<span class="mentioned-person"><%= person.name %></span>

View File

@ -1,3 +1,8 @@
hello_alice_message_content:
record: hello_alice (Message)
name: content
body: <p>Hello, <%= ActionText::FixtureSet.attachment("people", :alice) %></p>
hello_world_review_content:
record: hello_world (Review)
name: content

View File

@ -1,2 +1,5 @@
hello_alice:
subject: "A message to Alice"
hello_world:
subject: "A greeting"

2
actiontext/test/fixtures/people.yml vendored Normal file
View File

@ -0,0 +1,2 @@
alice:
name: "Alice"

View File

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

View File

@ -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 %>
<div class="trix-content">
<%= yield %>
</div>
```
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 `<action-text-attachment>` 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 `<action-text-attachment>`
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 `<action-text-attachment>`
element that references the `User` instance's signed GlobalID:
```html
<p>Hello, <action-text-attachment sgid="BAh7CEkiCG…"></action-text-content>.</p>
```
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 %>
<span><%= image_tag user.avatar %> <%= user.name %></span>
```
The resulting HTML rendered by Aciton Text would look something like:
```html
<p>Hello, <action-text-attachment sgid="BAh7CEkiCG…"><span><img src="..."> Jane Doe</span></action-text-content>.</p>
```
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 %>
<span><%= image_tag user.avatar %> <%= user.name %></span>
```
To integrate with Aciton Text `<action-text-attachment>` 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`:

View File

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