aboutsummaryrefslogtreecommitdiffstats
path: root/guides
diff options
context:
space:
mode:
Diffstat (limited to 'guides')
-rw-r--r--guides/source/6_0_release_notes.md16
-rw-r--r--guides/source/action_cable_overview.md272
-rw-r--r--guides/source/action_text_overview.md99
-rw-r--r--guides/source/documents.yaml5
-rw-r--r--guides/source/engines.md2
5 files changed, 282 insertions, 112 deletions
diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md
index 9716132156..6f1db126c3 100644
--- a/guides/source/6_0_release_notes.md
+++ b/guides/source/6_0_release_notes.md
@@ -6,6 +6,7 @@ Ruby on Rails 6.0 Release Notes
Highlights in Rails 6.0:
* Action Mailbox
+* Action Text
* Parallel Testing
These release notes cover only the major changes. To learn about various bug
@@ -37,6 +38,21 @@ Major Features
to route incoming emails to controller-like mailboxes.
You can read more about Action Mailbox in the [Action Mailbox Basics](action_mailbox_basics.html) guide.
+### Action Text
+
+[Pull Request](https://github.com/rails/rails/pull/34873)
+
+[Action Text](https://github.com/rails/rails/tree/6-0-stable/actiontext)
+brings rich text content and editing to Rails. It includes
+the [Trix editor](https://trix-editor.org) that handles everything from formatting
+to links to quotes to lists to embedded images and galleries.
+The rich text content generated by the Trix editor is saved in its own
+RichText model that's associated with any existing Active Record model in the application.
+Any embedded images (or other attachments) are automatically stored using
+Active Storage and associated with the included RichText model.
+
+You can read more about Action Text in the [Action Text Overview](action_text_overview.html) guide.
+
### Parallel Testing
[Pull Request](https://github.com/rails/rails/pull/31900)
diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md
index 2f602c3e0a..7809607574 100644
--- a/guides/source/action_cable_overview.md
+++ b/guides/source/action_cable_overview.md
@@ -147,16 +147,13 @@ established using the following JavaScript, which is generated by default by Rai
#### Connect Consumer
```js
-// app/assets/javascripts/cable.js
-//= require action_cable
-//= require_self
-//= require_tree ./channels
+// app/javascript/channels/consumer.js
+// Action Cable provides the framework to deal with WebSockets in Rails.
+// You can generate new channels where WebSocket features live using the `rails generate channel` command.
-(function() {
- this.App || (this.App = {});
+import ActionCable from "actioncable"
- App.cable = ActionCable.createConsumer();
-}).call(this);
+export default ActionCable.createConsumer()
```
This will ready a consumer that'll connect against `/cable` on your server by default.
@@ -167,12 +164,16 @@ you're interested in having.
A consumer becomes a subscriber by creating a subscription to a given channel:
-```coffeescript
-# app/assets/javascripts/cable/subscriptions/chat.coffee
-App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }
+```js
+// app/javascript/cable/subscriptions/chat_channel.js
+import consumer from "./consumer"
+
+consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" })
+
+// app/javascript/cable/subscriptions/appearance_channel.js
+import consumer from "./consumer"
-# app/assets/javascripts/cable/subscriptions/appearance.coffee
-App.cable.subscriptions.create { channel: "AppearanceChannel" }
+consumer.subscriptions.create({ channel: "AppearanceChannel" })
```
While this creates the subscription, the functionality needed to respond to
@@ -181,9 +182,12 @@ received data will be described later on.
A consumer can act as a subscriber to a given channel any number of times. For
example, a consumer could subscribe to multiple chat rooms at the same time:
-```coffeescript
-App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" }
-App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" }
+```js
+// app/javascript/cable/subscriptions/chat_channel.js
+import consumer from "./consumer"
+
+consumer.subscriptions.create({ channel: "ChatChannel", room: "1st Room" })
+consumer.subscriptions.create({ channel: "ChatChannel", room: "2nd Room" })
```
## Client-Server Interactions
@@ -256,24 +260,31 @@ When a consumer is subscribed to a channel, they act as a subscriber. This
connection is called a subscription. Incoming messages are then routed to
these channel subscriptions based on an identifier sent by the cable consumer.
-```coffeescript
-# app/assets/javascripts/cable/subscriptions/chat.coffee
-# Assumes you've already requested the right to send web notifications
-App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
- received: (data) ->
- @appendLine(data)
-
- appendLine: (data) ->
- html = @createLine(data)
- $("[data-chat-room='Best Room']").append(html)
-
- createLine: (data) ->
- """
- <article class="chat-line">
- <span class="speaker">#{data["sent_by"]}</span>
- <span class="body">#{data["body"]}</span>
- </article>
- """
+```js
+// app/javascript/cable/subscriptions/chat_channel.js
+// Assumes you've already requested the right to send web notifications
+import consumer from "./consumer"
+
+consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
+ received(data) {
+ this.appendLine(data)
+ },
+
+ appendLine(data) {
+ const html = this.createLine(data)
+ const element = document.querySelector("[data-chat-room='Best Room']")
+ element.insertAdjacentHTML("beforeend", html)
+ },
+
+ createLine(data) {
+ return `
+ <article class="chat-line">
+ <span class="speaker">${data["sent_by"]}</span>
+ <span class="body">${data["body"]}</span>
+ </article>
+ `
+ }
+})
```
### Passing Parameters to Channels
@@ -293,23 +304,30 @@ end
An object passed as the first argument to `subscriptions.create` becomes the
params hash in the cable channel. The keyword `channel` is required:
-```coffeescript
-# app/assets/javascripts/cable/subscriptions/chat.coffee
-App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
- received: (data) ->
- @appendLine(data)
-
- appendLine: (data) ->
- html = @createLine(data)
- $("[data-chat-room='Best Room']").append(html)
-
- createLine: (data) ->
- """
- <article class="chat-line">
- <span class="speaker">#{data["sent_by"]}</span>
- <span class="body">#{data["body"]}</span>
- </article>
- """
+```js
+// app/javascript/cable/subscriptions/chat_channel.js
+import consumer from "./consumer"
+
+consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
+ received(data) {
+ this.appendLine(data)
+ },
+
+ appendLine(data) {
+ const html = this.createLine(data)
+ const element = document.querySelector("[data-chat-room='Best Room']")
+ element.insertAdjacentHTML("beforeend", html)
+ },
+
+ createLine(data) {
+ return `
+ <article class="chat-line">
+ <span class="speaker">${data["sent_by"]}</span>
+ <span class="body">${data["body"]}</span>
+ </article>
+ `
+ }
+})
```
```ruby
@@ -340,13 +358,17 @@ class ChatChannel < ApplicationCable::Channel
end
```
-```coffeescript
-# app/assets/javascripts/cable/subscriptions/chat.coffee
-App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
- received: (data) ->
- # data => { sent_by: "Paul", body: "This is a cool chat app." }
+```js
+// app/javascript/cable/subscriptions/chat_channel.js
+import consumer from "./consumer"
+
+const chatChannel = consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
+ received(data) {
+ // data => { sent_by: "Paul", body: "This is a cool chat app." }
+ }
+}
-App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
+chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
```
The rebroadcast will be received by all connected clients, _including_ the
@@ -396,46 +418,69 @@ appear/disappear API could be backed by Redis, a database, or whatever else.
Create the client-side appearance channel subscription:
-```coffeescript
-# app/assets/javascripts/cable/subscriptions/appearance.coffee
-App.cable.subscriptions.create "AppearanceChannel",
- # Called when the subscription is ready for use on the server.
- connected: ->
- @install()
- @appear()
-
- # Called when the WebSocket connection is closed.
- disconnected: ->
- @uninstall()
-
- # Called when the subscription is rejected by the server.
- rejected: ->
- @uninstall()
-
- appear: ->
- # Calls `AppearanceChannel#appear(data)` on the server.
- @perform("appear", appearing_on: $("main").data("appearing-on"))
-
- away: ->
- # Calls `AppearanceChannel#away` on the server.
- @perform("away")
-
-
- buttonSelector = "[data-behavior~=appear_away]"
-
- install: ->
- $(document).on "turbolinks:load.appearance", =>
- @appear()
-
- $(document).on "click.appearance", buttonSelector, =>
- @away()
- false
-
- $(buttonSelector).show()
-
- uninstall: ->
- $(document).off(".appearance")
- $(buttonSelector).hide()
+```js
+// app/javascript/cable/subscriptions/appearance_channel.js
+import consumer from "./consumer"
+
+consumer.subscriptions.create("AppearanceChannel", {
+ // Called once when the subscription is created.
+ initialized() {
+ this.update = this.update.bind(this)
+ },
+
+ // Called when the subscription is ready for use on the server.
+ connected() {
+ this.install()
+ this.update()
+ },
+
+ // Called when the WebSocket connection is closed.
+ disconnected() {
+ this.uninstall()
+ },
+
+ // Called when the subscription is rejected by the server.
+ rejected() {
+ this.uninstall()
+ },
+
+ update() {
+ this.documentIsActive ? this.appear() : this.away()
+ },
+
+ appear() {
+ // Calls `AppearanceChannel#appear(data)` on the server.
+ this.perform("appear", { appearing_on: this.appearingOn })
+ },
+
+ away() {
+ // Calls `AppearanceChannel#away` on the server.
+ this.perform("away")
+ },
+
+ install() {
+ window.addEventListener("focus", this.update)
+ window.addEventListener("blur", this.update)
+ document.addEventListener("turbolinks:load", this.update)
+ document.addEventListener("visibilitychange", this.update)
+ },
+
+ uninstall() {
+ window.removeEventListener("focus", this.update)
+ window.removeEventListener("blur", this.update)
+ document.removeEventListener("turbolinks:load", this.update)
+ document.removeEventListener("visibilitychange", this.update)
+ },
+
+ get documentIsActive() {
+ return document.visibilityState == "visible" && document.hasFocus()
+ },
+
+ get appearingOn() {
+ const element = document.querySelector("[data-appearing-on]")
+ return element ? element.getAttribute("data-appearing-on") : null
+ }
+})
```
##### Client-Server Interaction
@@ -445,16 +490,16 @@ ActionCable.createConsumer("ws://cable.example.com")`. (`cable.js`). The
**Server** identifies this connection by `current_user`.
2. **Client** subscribes to the appearance channel via
-`App.cable.subscriptions.create(channel: "AppearanceChannel")`. (`appearance.coffee`)
+`consumer.subscriptions.create({ channel: "AppearanceChannel" })`. (`appearance_channel.js`)
3. **Server** recognizes a new subscription has been initiated for the
appearance channel and runs its `subscribed` callback, calling the `appear`
method on `current_user`. (`appearance_channel.rb`)
4. **Client** recognizes that a subscription has been established and calls
-`connected` (`appearance.coffee`) which in turn calls `@install` and `@appear`.
-`@appear` calls `AppearanceChannel#appear(data)` on the server, and supplies a
-data hash of `{ appearing_on: $("main").data("appearing-on") }`. This is
+`connected` (`appearance_channel.js`) which in turn calls `install` and `appear`.
+`appear` calls `AppearanceChannel#appear(data)` on the server, and supplies a
+data hash of `{ appearing_on: this.appearingOn }`. This is
possible because the server-side channel instance automatically exposes all
public methods declared on the class (minus the callbacks), so that these can be
reached as remote procedure calls via a subscription's `perform` method.
@@ -488,13 +533,17 @@ end
Create the client-side web notifications channel subscription:
-```coffeescript
-# app/assets/javascripts/cable/subscriptions/web_notifications.coffee
-# Client-side which assumes you've already requested
-# the right to send web notifications.
-App.cable.subscriptions.create "WebNotificationsChannel",
- received: (data) ->
- new Notification data["title"], body: data["body"]
+```js
+// app/javascript/cable/subscriptions/web_notifications_channel.js
+// Client-side which assumes you've already requested
+// the right to send web notifications.
+import consumer from "./consumer"
+
+consumer.subscriptions.create("WebNotificationsChannel", {
+ received(data) {
+ new Notification(data["title"], body: data["body"])
+ }
+})
```
Broadcast content to a web notification channel instance from elsewhere in your
@@ -629,10 +678,9 @@ class Application < Rails::Application
end
```
-You can use `App.cable = ActionCable.createConsumer()` to connect to the cable
-server if `action_cable_meta_tag` is invoked in the layout. A custom path is
-specified as first argument to `createConsumer` (e.g. `App.cable =
-ActionCable.createConsumer("/websocket")`).
+You can use `ActionCable.createConsumer()` to connect to the cable
+server if `action_cable_meta_tag` is invoked in the layout. Otherwise, A path is
+specified as first argument to `createConsumer` (e.g. `ActionCable.createConsumer("/websocket")`).
For every instance of your server you create and for every worker your server
spawns, you will also have a new instance of Action Cable, but the use of Redis
diff --git a/guides/source/action_text_overview.md b/guides/source/action_text_overview.md
new file mode 100644
index 0000000000..08ec35e52a
--- /dev/null
+++ b/guides/source/action_text_overview.md
@@ -0,0 +1,99 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
+
+Action Text Overview
+====================
+
+This guide provides you with all you need to get started in handling
+rich text content.
+
+After reading this guide, you will know:
+
+* How to configure Action Text.
+* How to handle rich text content.
+* How to style rich text content.
+
+--------------------------------------------------------------------------------
+
+Introduction
+------------
+
+Action Text brings rich text content and editing to Rails. It includes
+the [Trix editor](https://trix-editor.org) that handles everything from formatting
+to links to quotes to lists to embedded images and galleries.
+The rich text content generated by the Trix editor is saved in its own
+RichText model that's associated with any existing Active Record model in the application.
+Any embedded images (or other attachments) are automatically stored using
+Active Storage and associated with the included RichText model.
+
+## Trix compared to other rich text editors
+
+Most WYSIWYG editors are wrappers around HTML’s `contenteditable` and `execCommand` APIs,
+designed by Microsoft to support live editing of web pages in Internet Explorer 5.5,
+and [eventually reverse-engineered](https://blog.whatwg.org/the-road-to-html-5-contenteditable#history)
+and copied by other browsers.
+
+Because these APIs were never fully specified or documented,
+and because WYSIWYG HTML editors are enormous in scope, each
+browser's implementation has its own set of bugs and quirks,
+and JavaScript developers are left to resolve the inconsistencies.
+
+Trix sidesteps these inconsistencies by treating contenteditable
+as an I/O device: when input makes its way to the editor, Trix converts that input
+into an editing operation on its internal document model, then re-renders
+that document back into the editor. This gives Trix complete control over what
+happens after every keystroke, and avoids the need to use execCommand at all.
+
+## Installation
+
+Run `rails action_text:install` to add the Yarn package and copy over the necessary migration.
+
+## Examples
+
+Adding a rich text field to an existing model:
+
+```ruby
+# app/models/message.rb
+class Message < ApplicationRecord
+ has_rich_text :content
+end
+```
+
+Then refer to this field in the form for the model:
+
+```erb
+<%# app/views/messages/_form.html.erb %>
+<%= form_with(model: message) do |form| %>
+ <div class="field">
+ <%= form.label :content %>
+ <%= form.rich_text_area :content %>
+ </div>
+<% end %>
+```
+
+And finally display the sanitized rich text on a page:
+
+```erb
+<%= @message.content %>
+```
+
+To accept the rich text content, all you have to do is permit the referenced attribute:
+
+```ruby
+class MessagesController < ApplicationController
+ def create
+ message = Message.create! params.require(:message).permit(:title, :content)
+ redirect_to message
+ end
+end
+```
+
+## 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.css` 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.
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index 0f836bdf48..25e4fdb4e6 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -81,6 +81,11 @@
url: action_mailbox_basics.html
description: This guide describes how to use Action Mailbox to receive emails.
-
+ name: Action Text Overview
+ work_in_progress: true
+ url: action_text_overview.html
+ description: This guide describes how to use Action Text to handle rich text content.
+ -
name: Active Job Basics
url: active_job_basics.html
description: This guide provides you with all you need to get started creating, enqueuing, and executing background jobs.
diff --git a/guides/source/engines.md b/guides/source/engines.md
index bd0542ffc1..c4829299ca 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -1510,6 +1510,8 @@ To hook into the initialization process of one of the following classes use the
| `ActionMailbox::TestCase` | `action_mailbox_test_case` |
| `ActionMailer::Base` | `action_mailer` |
| `ActionMailer::TestCase` | `action_mailer_test_case` |
+| `ActionText::Content` | `action_text_content` |
+| `ActionText::RichText` | `action_text_rich_text` |
| `ActionView::Base` | `action_view` |
| `ActionView::TestCase` | `action_view_test_case` |
| `ActiveJob::Base` | `active_job` |