Skip to content

Commit 93fbd3d

Browse files
committed
Add hooks to integrate with Action Text
One major challenge that applications face when integrating with morphing-powered Page Refreshes involves `<trix-editor>` elements rendered by Action Text. The emergent guidance instructs applications to skip morphing by marking the `<trix-editor>` as permanent. This guidance, while correct, does not encapsulate the entire story. In the case of Action Text-rendered `<trix-editor>` elements, applications might invoke `form.rich_text_area` with `data: {turbo_permanent: true}`, expecting for the presence of `trix-editor[data-turbo-permanent]` to be sufficient. However, to achieve the intended behavior, applications must *nest* their `<trix-editor>` elements *within* an element with `[data-turbo-permanent]` (a `<div>`, a `<fieldset>`, etc.). This provides a container for the `trix-editor` to inject its associated `<input>` and `<trix-toolbar>` elements. A `<trix-editor>` element will insert an `<input type="hidden">` element and a `<trix-toolbar>` element when they are absent on connect. Applications can skip a Trix-manage injection by rendering those either (or both) elements ahead of time, then associating them to the `trix-editor` through `[input]` and `[toolbar]` attributes (respectively). Action Text skips the `<input>` injection step by rendering and associating an Action View-rendered `<input type="hidden">` attribute with the appropriate attributes. No matter how the `<input>` connects to the document, it's important for it to keep its `[data-turbo-permanent]` synchronized with the `trix-editor`'s attribute in order to tolerate a morph. Since Page Refreshes, Morphing, and permanence are all Turbo concepts, and Action Text is a Rails framework, `turbo-rails` feels like the most appropriate codebase (out of `rails`, `trix`, `turbo`, and `turbo-rails`) to house the integration.
1 parent 1aa7ba9 commit 93fbd3d

File tree

14 files changed

+113
-4
lines changed

14 files changed

+113
-4
lines changed

app/assets/javascripts/turbo.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5430,10 +5430,35 @@ function isBodyInit(body) {
54305430
return body instanceof FormData || body instanceof URLSearchParams;
54315431
}
54325432

5433+
function observeAttributes(element, attributeFilter, callback) {
5434+
const observer = new MutationObserver((mutations => {
5435+
mutations.forEach((({attributeName: attributeName, target: target}) => {
5436+
callback(target.getAttribute(attributeName), attributeName);
5437+
}));
5438+
}));
5439+
observer.observe(element, {
5440+
attributeFilter: attributeFilter
5441+
});
5442+
attributeFilter.forEach((attributeName => {
5443+
callback(element.getAttribute(attributeName), attributeName);
5444+
}));
5445+
return observer;
5446+
}
5447+
5448+
function observeTurboAttributes({target: target}) {
5449+
observeAttributes(target, [ "data-turbo-permanent" ], (value => {
5450+
if (target.inputElement) {
5451+
target.inputElement.toggleAttribute("data-turbo-permanent", value ?? false);
5452+
}
5453+
}));
5454+
}
5455+
54335456
window.Turbo = Turbo$1;
54345457

54355458
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
54365459

5460+
addEventListener("trix-initialize", observeTurboAttributes);
5461+
54375462
var adapters = {
54385463
logger: self.console,
54395464
WebSocket: self.WebSocket

app/assets/javascripts/turbo.min.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/javascripts/turbo.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/javascript/turbo/actiontext.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { observeAttributes } from "./util"
2+
3+
export function observeTurboAttributes({ target }) {
4+
observeAttributes(target, ["data-turbo-permanent"], (value) => {
5+
if (target.inputElement) {
6+
target.inputElement.toggleAttribute("data-turbo-permanent", value ?? false)
7+
}
8+
})
9+
}

app/javascript/turbo/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import * as cable from "./cable"
77
export { cable }
88

99
import { encodeMethodIntoRequestBody } from "./fetch_requests"
10+
import { observeTurboAttributes } from "./actiontext"
1011

1112
window.Turbo = Turbo
1213

1314
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody)
15+
addEventListener("trix-initialize", observeTurboAttributes)

app/javascript/turbo/util.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function observeAttributes(element, attributeFilter, callback) {
2+
const observer = new MutationObserver((mutations) => {
3+
mutations.forEach(({ attributeName, target }) => {
4+
callback(target.getAttribute(attributeName), attributeName)
5+
})
6+
})
7+
observer.observe(element, { attributeFilter })
8+
9+
attributeFilter.forEach((attributeName) => {
10+
callback(element.getAttribute(attributeName), attributeName)
11+
})
12+
13+
return observer
14+
}

test/dummy/app/controllers/messages_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
class MessagesController < ApplicationController
2+
def new
3+
@message = Message.new
4+
end
5+
26
def show
37
@message = Message.find(params[:id])
48

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
22
import "@hotwired/turbo-rails"
3+
import "trix"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<% if params[:method] == "morph" %>
2+
<% turbo_refreshes_with method: :morph %>
3+
<% end %>
4+
5+
<%= form_with model: @message do |form| %>
6+
<%= form.rich_text_area :content, data: {turbo_permanent: true} %>
7+
<% end %>
8+
9+
<%= link_to "Page Refresh", {}, data: {turbo_action: "replace"} %>

test/dummy/config/application.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
require "action_controller/railtie"
44
require "action_cable/engine"
5+
require "action_text/engine"
6+
require "active_storage/engine"
57
require "active_job/railtie"
68
require "active_model/railtie"
79
require "active_record/railtie"

test/dummy/config/environments/test.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
config.consider_all_requests_local = true
2525
config.action_controller.perform_caching = false
2626

27+
# Store uploaded files on the local file system in a temporary directory.
28+
config.active_storage.service = :test
29+
2730
# Raise exceptions instead of rendering exception templates.
2831
config.action_dispatch.show_exceptions = :none
2932

test/dummy/config/importmap.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
pin "application"
77
pin "@hotwired/turbo-rails", to: "turbo.js"
88
pin "@rails/actioncable", to: "actioncable.esm.js"
9+
pin "trix", to: "trix.js"

test/dummy/config/storage.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
test:
2+
service: Disk
3+
root: <%= Rails.root.join("tmp/storage") %>

test/system/actiontext_test.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require "application_system_test_case"
2+
3+
class ActionTextTest < ApplicationSystemTestCase
4+
test "forwards [data-turbo-permanent] from trix-editor to its input and trix-toolbar" do
5+
visit new_message_path
6+
7+
assert_trix_editor name: "message[content]", "data-turbo-permanent": true
8+
9+
find(:rich_text_area).execute_script <<~JS
10+
this.removeAttribute("data-turbo-permanent")
11+
JS
12+
13+
assert_trix_editor name: "message[content]", "data-turbo-permanent": false
14+
end
15+
16+
test "keeps a trix-editor[data-turbo-permanent] interactive through a Page Refresh" do
17+
visit new_message_path(method: "morph")
18+
fill_in_rich_text_area with: "Hello"
19+
click_link "Page Refresh"
20+
21+
assert_trix_editor name: "message[content]", with: /Hello/
22+
23+
fill_in_rich_text_area with: "Hello world"
24+
25+
assert_trix_editor name: "message[content]", with: /Hello world/
26+
end
27+
28+
def assert_trix_editor(name: nil, with: nil, **options, &block)
29+
trix_editor = find(:element, "trix-editor", **options, &block)
30+
31+
assert_field name, type: "hidden", with: with do |input|
32+
assert_equal trix_editor.evaluate_script("this.inputElement"), input
33+
assert_matches_selector input, :element, **options
34+
end
35+
end
36+
end

0 commit comments

Comments
 (0)