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:
parent
cc2e097fed
commit
3500571b43
|
@ -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.
|
* Add `with_all_rich_text` method to eager load all rich text associations on a model at once.
|
||||||
|
|
||||||
*Matt Swanson*, *DHH*
|
*Matt Swanson*, *DHH*
|
||||||
|
|
|
@ -22,17 +22,31 @@ module ActionText
|
||||||
content.render_attachments do |attachment|
|
content.render_attachments do |attachment|
|
||||||
unless attachment.in?(content.gallery_attachments)
|
unless attachment.in?(content.gallery_attachments)
|
||||||
attachment.node.tap do |node|
|
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
|
end
|
||||||
end.render_attachment_galleries do |attachment_gallery|
|
end.render_attachment_galleries do |attachment_gallery|
|
||||||
render(layout: attachment_gallery, object: attachment_gallery) do
|
render(layout: attachment_gallery, object: attachment_gallery) do
|
||||||
attachment_gallery.attachments.map do |attachment|
|
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
|
attachment.to_html
|
||||||
end.join.html_safe
|
end.join.html_safe
|
||||||
end.chomp
|
end.chomp
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<div class="trix-content">
|
|
||||||
<%= render_action_text_content(content) %>
|
|
||||||
</div>
|
|
|
@ -0,0 +1 @@
|
||||||
|
<%= render_action_text_content(content) %>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="trix-content">
|
||||||
|
<%= yield -%>
|
||||||
|
</div>
|
|
@ -71,6 +71,10 @@ module ActionText
|
||||||
to_partial_path
|
to_partial_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_attachable_partial_path
|
||||||
|
to_partial_path
|
||||||
|
end
|
||||||
|
|
||||||
def to_rich_text_attributes(attributes = {})
|
def to_rich_text_attributes(attributes = {})
|
||||||
attributes.dup.tap do |attrs|
|
attributes.dup.tap do |attrs|
|
||||||
attrs[:sgid] = attachable_sgid
|
attrs[:sgid] = attachable_sgid
|
||||||
|
|
|
@ -84,7 +84,11 @@ module ActionText
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_rendered_html_with_layout
|
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
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
|
|
|
@ -47,6 +47,9 @@ module ActionText
|
||||||
|
|
||||||
copy_file "#{GEM_ROOT}/app/views/active_storage/blobs/_blob.html.erb",
|
copy_file "#{GEM_ROOT}/app/views/active_storage/blobs/_blob.html.erb",
|
||||||
"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
|
end
|
||||||
|
|
||||||
def create_migrations
|
def create_migrations
|
||||||
|
|
|
@ -4,4 +4,8 @@ class Person < ApplicationRecord
|
||||||
def to_trix_content_attachment_partial_path
|
def to_trix_content_attachment_partial_path
|
||||||
"people/trix_content_attachment"
|
"people/trix_content_attachment"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_attachable_partial_path
|
||||||
|
"people/attachable"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<span class="mentioned-person"><%= person.name %></span>
|
|
@ -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:
|
hello_world_review_content:
|
||||||
record: hello_world (Review)
|
record: hello_world (Review)
|
||||||
name: content
|
name: content
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
|
hello_alice:
|
||||||
|
subject: "A message to Alice"
|
||||||
|
|
||||||
hello_world:
|
hello_world:
|
||||||
subject: "A greeting"
|
subject: "A greeting"
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
alice:
|
||||||
|
name: "Alice"
|
|
@ -31,4 +31,12 @@ class ActionText::ControllerRenderTest < ActionDispatch::IntegrationTest
|
||||||
get admin_message_path(message)
|
get admin_message_path(message)
|
||||||
assert_select "#content-html .trix-content .attachment--jpg"
|
assert_select "#content-html .trix-content .attachment--jpg"
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ After reading this guide, you will know:
|
||||||
|
|
||||||
* How to configure Action Text.
|
* How to configure Action Text.
|
||||||
* How to handle rich text content.
|
* 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";
|
@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
|
```ruby
|
||||||
# app/models/message.rb
|
# app/models/message.rb
|
||||||
|
@ -81,7 +81,7 @@ class Message < ApplicationRecord
|
||||||
end
|
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:
|
Then refer to this field in the form for the model:
|
||||||
|
|
||||||
|
@ -112,6 +112,112 @@ class MessagesController < ApplicationController
|
||||||
end
|
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
|
## 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:
|
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.
|
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
|
## 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`:
|
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`:
|
||||||
|
|
|
@ -60,6 +60,12 @@ class ActionText::Generators::InstallGeneratorTest < Rails::Generators::TestCase
|
||||||
assert_file "app/views/active_storage/blobs/_blob.html.erb"
|
assert_file "app/views/active_storage/blobs/_blob.html.erb"
|
||||||
end
|
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
|
test "creates migrations" do
|
||||||
run_generator_instance
|
run_generator_instance
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue