Repository navigation
Extract ActionText::Editor base class and ActionText::TrixEditor adapter - #51238
Conversation
|
This change aims to both introduce the If that's too ambitious, risky, or the code review diff is too noisy, I'm happy to open a separate PR to deprecate the methods, then rebase this branch off that PR. |
ActionText::Editor to support WYSIWYG EditorsActionText::Editor to support WYSIWYG Editors
45ed3fb to
9c32f81
Compare
What would be a use-case for having this kind of configuration for ActionText? I can understand the case for ActiveStorage having multiple different "services" (mirroring, different regions, public vs private files, etc) but struggling to think of a case where a single Rails app would use multiple different text editor libraries. In my own codebase we have different "instances" of trix (for things like removing some formatting, changing styles, etc) but those are configured at the view-layer or with options passed to the underlying library. |
I think one potential use case is migration from one editor to another. For example, this diff's migration would backfill existing records to set For some period of time, the application could serve assets for both editors and use the For some editors, there might not be any significant difference. Since the WYSIWYG landscape is vast, supporting an open-ended migration felt safest.
This is another case I was imagining. Some consumer-facing pages might require real-time collaboration features, while other pages (back of house administrative pages, for example) might have different needs. While I agree with your point that using a consistent tool across the entire application would be the simplest approach, the spirit of this proposal is to provide flexibility when that isn't possible (for a variety of reasons, both in and out of teams' control). |
|
My gut reaction was that I was expecting this to be configured/designed more like |
This is an angle I hadn't considered. I think supporting has_rich_text :body # editor defaults to `ActionText::RichText.editor`
has_rich_text :body, editor: :trix
has_rich_text :body, editor: ActionText::RichText.editors[:trix]
has_rich_text :body, editor: ->(rich_text) { rich_text.method_to_determine_editor_during_migration } |
|
We migrated from Trix to TipTap over the course of 2023. Our app pre-dated ActionText but this type of PR could help other apps in a similar situation to ours:
|
|
@dhh shedding some light on this PR since you've mentioned working on a possible Markdown new editor for Rails and you were open to this idea at first. |
9c32f81 to
d9621a9
Compare
ActionText::Editor to support WYSIWYG EditorsActionText::TrixEditor
jorgemanrubia
left a comment
There was a problem hiding this comment.
This looks amazing @seanpdoyle 👏
af66fc2 to
0c1332c
Compare
0c1332c to
0a89f39
Compare
| options[:data][:direct_upload_url] ||= main_app.rails_direct_uploads_url | ||
| options[:data][:blob_url_template] ||= main_app.rails_service_blob_url(":signed_id", ":filename") | ||
|
|
||
| editor_tag = content_tag("trix-editor", "", options) |
There was a problem hiding this comment.
I would leave this out of this PR to keep the size under control, but some thoughts about multi-editor support.
To support using multiple editors in the same app, we should be able to create thread-safe contexts where you can configure the editor. So that here, in the helpers you use in your app, we could support an editor: option that let you opt out the default. With that option, you could create contexts where ActionText::RichText.editor is resolved differently. So, some API like:
ActionText.with_editor :trix do
# ...
end
ActionText.with_editor :lexxy do
# ...
endThat way, within the helpers, we could set the right "editor context" to interpret the editor param. To do this, we would need to make the editor variable a thread one.
We need this because through the code we rely on ActionText::RichText.editor resolving to the configured editor, which is handy.
There was a problem hiding this comment.
Could this involve an implementation that leverages Object#with like:
module ActionText
singleton_class.delegate :editor, :editor=, to: "ActionText::RichText"
def self.with_editor(editor_name)
with editor: ActionText::RichText.editors.fetch(editor_name) do
yield
end
end
endI think the singleton_class.delegate bit is necessary since ActionText::RichText is an Active Record model, and Active Record overrides Object#with with ActiveRecord::QueryMethods.with for common-table expression support.
There was a problem hiding this comment.
@jorgemanrubia I'd declared the RichText.editors and RichText.editor class attributes because I thought they read as self-descriptive (since they read aloud as "rich text editors" and "rich text editor").
Other than hooking into an ActiveSupport.on_load(:action_text_rich_text) { … } block in the engine, there isn't much benefit to declaring the attributes on RichText, rather than ActionText itself.
Is that decision worth reconsidering before merging?
0a89f39 to
c8f75e9
Compare
c8f75e9 to
28f148c
Compare
|
This current changeset combines two units of work:
@jorgemanrubia now that the Editor interface feels more settled, I wonder if it would be more productive to split out the deprecation unit ( We can keep this PR intact to serve as the pluggable editor unit of work ( What do you think? |
jorgemanrubia
left a comment
There was a problem hiding this comment.
@seanpdoyle this looks great 👍👍. I think in a single unit is fine.
Thanks for the fantastic work here.
7615c1f to
9778d1d
Compare
ActionText::TrixEditorActionText::Editor base class and ActionText::TrixEditor adapter
9778d1d to
27fe49a
Compare
| end | ||
|
|
||
| def render_in(view_context) | ||
| options[:class] ||= "#{editor_name}-content" |
There was a problem hiding this comment.
@seanpdoyle I am testing the new adapterized approach in Lexxy here](https://github.com/basecamp/lexxy/compare/rails-adapter).
The only thing I have missed is that I think we should inject the name attribute here by default, as it is a reasonable expectation: that the editor behaves like a first-class form control and can get a name attribute. I had to add it for Lexxy like this. What do you think?
There was a problem hiding this comment.
Or, alternatively, we could avoid deleting the property here https://github.com/rails/rails/pull/51238/files#diff-60630514a64ceee9e6d1e641d893af347eae7704d5262903407f36a83125a2deR65. Then we could remove for the Trix case in the adapter, which I think would be more the exception than the norm.
There was a problem hiding this comment.
@jorgemanrubia I've pushed up a commit to do so.
There was a problem hiding this comment.
Nice thanks @seanpdoyle, could you please rebase on main to resolve the conflicts. We are merging this one...!
There was a problem hiding this comment.
For reference, this is how the Lexxy adapter would look: https://github.com/basecamp/lexxy/pull/484/files#diff-299a739d1eca5793af10f6d15a5823ede8684a7b936ac31c929677f851411f0f. I think this is a pretty sweet setup for Rails. Thanks again for the work @seanpdoyle
Related to [rails/actiontext#41](rails/actiontext#41) The introduction of the `ActionText::TrixEditor` class enables the deprecation of a variety of class- and instance-level methods across the `ActionText` namespace. Action Text benefits from deprecating methods in order to reduce its public API. There are two categories of deprecations in this changeset: `module`/`class` deprecations and method deprecations. The `module` and `class` deprecations target modules that are internal and "private" in intent, but public in Ruby and not marked as `:nodoc:`. They're either particular to Trix, or their responsibilities have been given to `ActionText::Editor`. The method deprecations also target methods that are internal and "private" in intent, but `public` in Ruby and not marked as `:nodoc:`. In general, determining whether or not to deprecate a method hinged on whether its name or arguments mentioned `"trix"`, *or* if it lacked method-level documentation or comments of any kind. When possible, the previous code paths have been deprecated *and* re-implemented in terms of an `ActionText::TrixEditor` instance. Additions --- The main aim of this changeset is to provide a single, extensible entrypoint for third-party editors (that are not Trix) to integrate with Action Text. The initial responsibilities of the `ActionText::TrixEditor` have been determined by consolidating a variety of (class- and instance-) methods across a variety of classes to reduce the number of touchpoints for applications and engines that provide third-party editor integrations. The vast majority of its method definitions are direct copy-and-pastes from their original sources. Any method that previously mentioned `trix` in its name or argument list has replaced and generalized that occurrence with `editor`. Classes that inherit from `ActionText::Editor` can override two methods that accept and return `Fragment` instances: * `Editor#as_canonical` method accepts an editor-sourced `ActionText::Fragment` instance, transforms it, then returns an `ActionText::Fragment` instance to be stored. * `Editor#as_editable` method accepts a storage-sourced `ActionText::Fragment` instance, transforms it, then returns an `ActionText::Fragment` instance to be edited. The interface utilizes `Fragment` instances rather than `Content` or String instances. The `Fragment` class serves as an agnostic layer on top of HTML and Plain Text string manipulation through Nokogiri. It has a simple interface of transforms: * `find_all(selector)` * `update(&block)` * `replace(selector, &block)` Accepting **and** returning a transformation-ready `Fragment` enables a more consistent adapter interface. Changes --- *These changes aim to be considered implementation details, and intend to remain "private" from an API perspective.* Future commits can be made to expand upon and document the responsibilities of the adapter's interface.
f046415 to
558d839
Compare
Rails 8.2 removed the `dom_id` method from the ActionText tag in rails/rails#51238, breaking Lexxy's override. Use `@template_object.dom_id` instead which works in all releases. (`@template_object` is the view context and always has access to `RecordIdentifier` helpers, including `dom_id`.) This is obviated by #484 but allows Rails 8.2 apps to not break before adopting the adapter setup.
Rails does post-upload variant processing using the same file upload in rails/rails#56327 Unrelated Lexxy bump: Pulls in basecamp/lexxy#493 to work around `dom_id` removal from ActionText tag in rails/rails#51238
Rails does post-upload variant processing using the same file upload in rails/rails#56327 Unrelated Lexxy bump: Pulls in basecamp/lexxy#493 to work around `dom_id` removal from ActionText tag in rails/rails#51238
Related to [rails/actiontext#41](rails/actiontext#41) The introduction of the `ActionText::TrixEditor` class enables the deprecation of a variety of class- and instance-level methods across the `ActionText` namespace. Action Text benefits from deprecating methods in order to reduce its public API. There are two categories of deprecations in this changeset: `module`/`class` deprecations and method deprecations. The `module` and `class` deprecations target modules that are internal and "private" in intent, but public in Ruby and not marked as `:nodoc:`. They're either particular to Trix, or their responsibilities have been given to `ActionText::Editor`. The method deprecations also target methods that are internal and "private" in intent, but `public` in Ruby and not marked as `:nodoc:`. In general, determining whether or not to deprecate a method hinged on whether its name or arguments mentioned `"trix"`, *or* if it lacked method-level documentation or comments of any kind. When possible, the previous code paths have been deprecated *and* re-implemented in terms of an `ActionText::TrixEditor` instance. Additions --- The main aim of this changeset is to provide a single, extensible entrypoint for third-party editors (that are not Trix) to integrate with Action Text. The initial responsibilities of the `ActionText::TrixEditor` have been determined by consolidating a variety of (class- and instance-) methods across a variety of classes to reduce the number of touchpoints for applications and engines that provide third-party editor integrations. The vast majority of its method definitions are direct copy-and-pastes from their original sources. Any method that previously mentioned `trix` in its name or argument list has replaced and generalized that occurrence with `editor`. Classes that inherit from `ActionText::Editor` can override two methods that accept and return `Fragment` instances: * `Editor#as_canonical` method accepts an editor-sourced `ActionText::Fragment` instance, transforms it, then returns an `ActionText::Fragment` instance to be stored. * `Editor#as_editable` method accepts a storage-sourced `ActionText::Fragment` instance, transforms it, then returns an `ActionText::Fragment` instance to be edited. The interface utilizes `Fragment` instances rather than `Content` or String instances. The `Fragment` class serves as an agnostic layer on top of HTML and Plain Text string manipulation through Nokogiri. It has a simple interface of transforms: * `find_all(selector)` * `update(&block)` * `replace(selector, &block)` Accepting **and** returning a transformation-ready `Fragment` enables a more consistent adapter interface. Changes --- *These changes aim to be considered implementation details, and intend to remain "private" from an API perspective.* Future commits can be made to expand upon and document the responsibilities of the adapter's interface.
New applications, and applications that set `config.load_defaults "8.2"`, render `rich_textarea` with Lexxy instead of Trix. Upgraded applications keep Trix until they opt in. The change builds on the editor adapter from rails#51238: * `config.action_text.editor` still defaults to `:trix`, and becomes `:lexxy` in `load_defaults "8.2"`, with a commented entry in `new_framework_defaults_8_2.rb`. * Action Text registers a Lexxy editor adapter next to the Trix one, so applications on the new defaults boot before the `lexxy` gem is added. * `bin/rails action_text:install` installs the configured editor, or the one passed with `--editor`. For Lexxy, it adds the `lexxy` gem, its JavaScript and stylesheet, and the `lexxy-content` layout class. * The Action Text dummy application pins Trix, which its forms and system tests exercise. Stored rich text needs no migration: Lexxy loads content saved by Trix.
Related to rails/actiontext#41
The introduction of the
ActionText::TrixEditorclass enables thedeprecation of a variety of class- and instance-level methods across the
ActionTextnamespace. Action Text benefits from deprecating methods inorder to reduce its public API.
There are two categories of deprecations in this changeset:
module/classdeprecations and method deprecations.The
moduleandclassdeprecations target modules that are internaland "private" in intent, but public in Ruby and not marked as
:nodoc:.They're either particular to Trix, or their responsibilities have been
given to
ActionText::Editor.The method deprecations also target methods that are internal and
"private" in intent, but
publicin Ruby and not marked as:nodoc:.In general, determining whether or not to deprecate a method hinged on
whether its name or arguments mentioned
"trix", or if it lackedmethod-level documentation or comments of any kind.
When possible, the previous code paths have been deprecated and
re-implemented in terms of an
ActionText::TrixEditorinstance.Additions
The main aim of this changeset is to provide a single, extensible
entrypoint for third-party editors (that are not Trix) to integrate with
Action Text.
The initial responsibilities of the
ActionText::TrixEditorhave beendetermined by consolidating a variety of (class- and instance-) methods
across a variety of classes to reduce the number of touchpoints for
applications and engines that provide third-party editor integrations.
The vast majority of its method definitions are direct copy-and-pastes
from their original sources. Any method that previously mentioned
trixin its name or argument list has replaced and generalized that
occurrence with
editor.Changes
These changes aim to be considered implementation details, and intend
to remain "private" from an API perspective. Future commits can be made
to expand upon and document the responsibilities of the adapter's
interface.
Checklist
Before submitting the PR make sure the following are checked:
[Fix #issue-number]