1. What is Hotwire?
Hotwire is a suite of front-end libraries for building rich, high-fidelity, and modern web applications without the complexities of a single-page application.
Hotwire for the web consists of two libraries:
Rails integrates with Turbo and Stimulus using the turbo-rails
and stimulus-rails gems. They're installed by default in all new Rails
apps. Install them in existing applications using:
$ bundle add turbo-rails
$ bin/rails turbo:install
$ bundle add stimulus-rails
$ bin/rails stimulus:install
Hotwire also includes Hotwire Native. This library is focused on native mobile applications and hence we won't cover it in this guide.
This guide focuses on Rails' integration with Turbo and Stimulus, and not on the libraries themselves. If you're unfamiliar with them, read the Hotwire docs before continuing with this guide.
2. Turbo
Turbo is the nucleus of Hotwire. It consists of 3 parts: Turbo Drive, Turbo Frames, and Turbo Streams.
Turbo Drive accelerates links and form submissions by intercepting
the relevant events and making those HTTP requests using JavaScript. It then
swaps out the document's <body> element with the one from the response,
eliminating the need for full page loads.
Turbo Frames allow you to decompose pages into independent contexts where navigation and updates can occur without affecting the rest of the page.
Turbo Streams are used to make fine-grained, targeted updates to specific
DOM elements using a range of actions such as: prepend, append, replace,
and more.
See the Turbo handbook for more information on how Turbo works and its features.
2.1. Turbo Drive
Turbo Drive largely works
automatically when imported into your HTML document. It offers a
few configuration options, and the ability to define data- attributes
and <meta> tags in your HTML to customize behavior.
See the handbook and
reference for further details.
The turbo-rails gem
provides helper methods which define <meta> tags to customize Turbo
Drive on specific pages.
All these helpers use provide :head to render
the <meta> tag so they can be used in your views — just ensure
<%= yield :head %> is declared in your layout's <head> so the tags
are injected correctly.
Control a page's caching behavior
by setting a turbo-cache-control meta tag:
<%# app/views/posts/show.html.erb %>
<%# Renders <meta name="turbo-cache-control" content="no-cache"> %>
<%= turbo_exempts_page_from_cache %>
<%# Renders <meta name="turbo-cache-control" content="no-preview"> %>
<%= turbo_exempts_page_from_preview %>
Force a full page reload for specific pages
with a turbo-visit-control meta tag:
<%# app/views/posts/show.html.erb %>
<%# Renders <meta name="turbo-visit-control" content="reload"> %>
<%= turbo_page_requires_reload %>
Configure morphing page refreshes with:
<%# Renders: %>
<%# <meta name="turbo-refresh-method" content="morph"> %>
<%# <meta name="turbo-refresh-scroll" content="preserve"> %>
<%= turbo_refreshes_with(method: :morph, scroll: :preserve) %>
View the source code for more details.
2.2. Turbo Frames
Turbo Frames use a
<turbo-frame> element to isolate parts of a web page into its own
navigation context, allowing it to be updated independently from the
rest of the page.
Declare a Turbo Frame using turbo_frame_tag:
<%= turbo_frame_tag dom_id(post) do %>
<div>
<%= link_to post.title, post_path(post) %>
</div>
<% end %>
which will render:
<turbo-frame id="post_1">
<div>
<a href="/posts/1">My first post</a>
</div>
</turbo-frame>
All Turbo Frame elements require a unique HTML id attribute,
which is used to match the content being replaced when requesting
new pages from the server.
An id based on one or more Active Record objects can be generated
using the dom_id or dom_target methods.
turbo_frame_tag implicitly calls dom_id — the above
example demonstrates it explicitly for clarity, but it may be simplified
as: turbo_frame_tag(post). dom_id will be called automatically under the
hood.
Options passed to turbo_frame_tag will be applied to the <turbo-frame>:
<%= turbo_frame_tag post src: edit_post_path(post), loading: :lazy, refresh: :morph %>
will render:
<turbo-frame loading="lazy" refresh="morph" id="post_1" src="/posts/1/edit"></turbo-frame>
2.3. Turbo Streams
Turbo Streams are used to
perform a series of actions (such as append, update, remove, replace
etc.) on specific DOM elements via a <turbo-stream> element. As soon as a
<turbo-stream> tag is added to the document, Turbo will execute it
and perform the action it defines.
Turbo Streams can be delivered as an HTTP response with the MIME type
text/vnd.turbo-stream.html (specified in the content-type HTTP header),
or using an alternative delivery mechanism such as WebSockets or
server-sent events.
turbo-rails provides helpers to create HTTP responses consisting of
Turbo Streams, as well as an integration with
Action Cable to broadcast Turbo Streams
from the server over WebSockets.
Here's an example showing how to render an ERB template containing Turbo Streams:
<%# app/views/posts/create.turbo_stream.erb %>
<%= turbo_stream.prepend("posts", partial: "posts/post", locals: { post: @post }) %>
<%= turbo_stream.replace("posts_title") do %>
<%= Post.count %> posts
<% end %>
# app/controllers/posts_controller.rb
# ...
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.turbo_stream
format.html { redirect_to @post, status: :see_other }
else
render :new, status: :unprocessable_entity
end
end
end
# ...
This action will render the following Turbo Stream response:
<turbo-stream action="prepend" target="posts">
<template>
<!-- posts/post.html.erb partial -->
</template>
</turbo-stream>
<turbo-stream action="replace" target="posts_title">
<template>
10 Posts
</template>
</turbo-stream>
The above approach is useful when defining multiple Turbo Streams. Alternatively, you can render a single Turbo Stream action within your controller:
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.turbo_stream do
# Renders:
# <turbo-stream action="prepend" target="posts">
# <template>
# <h2>My New Post</h2>
# </template>
# </turbo-stream>
render turbo_stream: turbo_stream.prepend("posts", helpers.tag.h2(@post.title))
end
format.html { redirect_to @post, status: :see_other }
else
render :new, status: :unprocessable_entity
end
end
end
2.3.1. Custom Stream Actions
Turbo allows the creation of custom actions for Streams. Consider the below example showing an action which adds a class to an element:
import { StreamActions } from "@hotwired/turbo"
StreamActions.add_class = function() {
let className = this.getAttribute("class")
this.targetElements.forEach(e => {
e.classList.add(className)
})
}
Since this is a custom action, turbo-rails doesn't define a helper method to
render it. It can be rendered using the generic tag helper:
<%= turbo_stream_action_tag(:add_class, target: :posts, class: "font-bold") %>
Alternatively, you can define your own custom helper:
# config/initializers/custom_turbo_streams.rb
ActiveSupport.on_load :turbo_streams_tag_builder do
def add_class(target, class_name)
turbo_stream_action_tag(
:add_class,
target: target,
class: class_name
)
end
end
The custom action can now be rendered with a more readable syntax:
<%= turbo_stream.add_class(:posts, "font-bold") %>
2.4. Content Security Policy
Rails supports the use of a Content Security Policy (CSP) along with a nonce to restrict the loading of external content such as scripts, stylesheets, or images. If you're unfamiliar with how a Content Security Policy works or what a nonce is, refer to the Security guide.
This section outlines the extra considerations required when using a CSP in Rails with Turbo.
Rails sets the CSP using an HTTP response header, and it is activated by the browser when an HTML document is loaded. Since Turbo intercepts all link clicks and form submissions to perform the HTTP request and update the page using JavaScript, the HTML document is never reloaded during page navigation. Hence, the CSP defined on the page's first load is active until the document is fully reloaded (by a page refresh, for example).
Due to this behavior, Turbo requires a nonce that is constant for the duration of a session — such as the session ID.
# config/initializers/content_security_policy.rb
# ...
# The session ID will be defined in the CSP, and be set as a
# `nonce` attribute in `<script>` elements, allowing them
# to execute.
config.content_security_policy_nonce_generator = ->(request) {
request.session.id.to_s
}
A user will not have a valid session for their first ever request to your app, resulting in an invalid nonce for that request. To work around this issue, you can use a randomly generated value and store it in the session.
# config/initializers/content_security_policy.rb
# ...
config.content_security_policy_nonce_generator = ->(request) {
request.session[:nonce] ||= SecureRandom.base64(16)
request.session[:nonce]
}
Generating a unique nonce for each request isn't compatible with Turbo because the HTML document isn't reloaded during navigation, meaning the CSP cannot be updated with a new request-specific nonce.
Rails renders the nonce in a meta tag in the document's head.
Turbo reads the nonce from this tag and applies it to every script element
in the content it loads. If an inline script without a valid nonce has been
injected into a page's content through an XSS vulnerability, it will be
blocked on a full page load, but will execute after a Turbo Drive
navigation to the same page.
This functionality can be disabled by excluding the csp_meta_tag
directive in your document's head, so the nonce isn't rendered within the
page.
Do not add 'strict-dynamic' to your CSP when using
Turbo. Turbo uses the createElement method to insert received script tags
into the document. 'strict-dynamic' allows scripts inserted in
this way to execute regardless of their nonce. If an attacker managed
to inject a script into a page that is navigated to using Turbo, the
'strict-dynamic' directive in your CSP would allow it to run even
without a nonce.
Further information can be found in the security guide.
3. Turbo Streams over Action Cable
Broadcasting Turbo Streams using Action Cable
requires the @hotwired/turbo-rails JavaScript package. It's installed by
default in all new Rails apps, and also when you run the
turbo:install task provided by the turbo-rails gem:
$ bin/rails turbo:install
Ensure that Action Cable is set up in your
application. It is recommended to create an ApplicationCable::Connection
class to authenticate Turbo Stream connections:
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = authenticate_user
end
private
def authenticate_user
# Authenticate the user using cookies.
# Reject the connection using `reject_unauthorized_connection`
# if authentication fails.
end
end
end
Turbo Streams can be received over an Action Cable connection by subscribing to broadcasts on a stream. This can be done in any view template:
<%= turbo_stream_from "feed" %>
Broadcast a Turbo Stream action to this stream using:
Turbo::StreamsChannel.broadcast_action_to(
"feed",
action: :append,
target: "feed_container",
partial: "feed/item",
locals: { feed_item: feed_item }
)
The above example broadcasts a Turbo Stream append action to the
feed stream. Clients subscribed to this stream will
receive the action and execute it, inserting the supplied partial into
the DOM element with the id feed_container.
3.1. Subscribing to Streams
turbo_stream_from renders a <turbo-cable-stream-source> element which uses the
Action Cable JavaScript library to create connections and subscribe to streams.
<%# Subscribes to a stream named `feed` %>
<%= turbo_stream_from "feed" %>
will render
<turbo-cable-stream-source
channel="Turbo::StreamsChannel"
signed-stream-name="ImZlZWQi--5ce5ecd99ed18f99c989afbb35bfae9f9caebdb8daa050c0ae57adf473be79cd">
</turbo-cable-stream-source>
The supplied stream name is Base64 encoded and signed using
ActiveSupport::MessageVerifier
to ensure it can't be tampered with.
3.1.1. Stream Names
The stream name must be a string, but other types of objects are often utilized
to generate the final string used. Objects used in the generation of
stream names must respond to to_gid_param or to_param. Hence, they're
usually Active Record objects, strings, or symbols. These are
called broadcastables or streamables.
Multiple arguments may be passed to turbo_stream_from to namespace the stream
name. In this case, to_gid_param or to_param will be called on each
object and then joined with a :.
<%= turbo_stream_from @product, :feed %>
will subscribe to a stream named:
"Z2lkOi8vcmFpbHMtZ3VpZGVzLWRlbW8vUHJvZHVjdC8x:feed"
where Z2lkOi8vcmFpbHMtZ3VpZGVzLWRlbW8vUHJvZHVjdC8x is the Base64 encoded
Global ID of the @product object.
The above stream name is presented in plain-text for demonstration —
however, when it is rendered to HTML using turbo_stream_from, it will
be signed as shown in the previous section.
3.1.2. Custom Channels
<turbo-cable-stream-source> uses the
Turbo::StreamsChannel
to manage subscriptions by default. This is a generic class provided by
the turbo-rails gem and doesn't run any authorization checks on the
resource before accepting subscription to a stream. It verifies the signed
stream name and then wires up the stream.
class Turbo::StreamsChannel < ActionCable::Channel::Base
extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
include Turbo::Streams::StreamName::ClassMethods
def subscribed
if stream_name = verified_stream_name_from_params
stream_from stream_name
else
reject
end
end
end
Since there is no authorization check, a user could theoretically subscribe to
another user's stream if they were able to acquire their signed stream name.
Prevent this issue by creating your own channel and authorizing the user's
permissions before allowing subscription. turbo-rails provides primitives
for verifying stream names in custom channels.
Consider the below example where we have a ChatRoomChannel used to broadcast
messages to its members:
# app/channels/chat_room_channel.rb
class ChatRoomChannel < ApplicationCable::Channel
extend Turbo::Streams::StreamName
include Turbo::Streams::StreamName::ClassMethods
def subscribed
if chat_room&.authorize(current_user)
stream_from stream_name
else
reject
end
end
private
def stream_name
@stream_name ||= verified_stream_name_from_params
end
def chat_room
@chat_room ||= GlobalID::Locator.locate(stream_name)
end
end
Specify this channel in your view:
<%# Renders: %>
<%# <turbo-cable-stream-source channel="ChatRoomChannel" signed-stream-name="..."></turbo-cable-stream-source> %>
<%= turbo_stream_from @chat_room, channel: "ChatRoomChannel" %>
The ChatRoomChannel will now manage subscriptions, and will check the
user's permissions before accepting the stream ensuring that broadcasts
are only delivered to authorized users.
3.2. Broadcasting to Streams
turbo-rails provides a plethora of helper methods to broadcast stream actions.
Under the hood, they all call ActionCable.server.broadcast.
A generic broadcast operation:
Turbo::StreamsChannel.broadcast_action_to(
@chat_room,
action: :append,
target: "messages_container",
partial: "chat_rooms/message",
locals: { message: message }
)
can be rewritten as an action specific broadcast operation:
Turbo::StreamsChannel.broadcast_append_to(
@chat_room,
target: "messages_container",
partial: "chat_rooms/message",
locals: { message: message }
)
You can also broadcast a Turbo Stream template containing multiple actions:
Turbo::StreamsChannel.broadcast_render_to(
@chat_room,
template: "chat_rooms/update" # Renders `app/views/chat_rooms/update.turbo_stream.erb`
)
or broadcast a refresh action which is useful for morphing:
Turbo::StreamsChannel.broadcast_refresh_to(@chat_room)
Even when using custom channels to handle subscriptions to Turbo Streams,
you can use the Turbo::StreamsChannel broadcast helpers to deliver the Stream
actions. It will compute the correct stream name based on the supplied
broadcastables.
All the above examples render templates and broadcast them synchronously.
They can be offloaded to a background job to improve performance by using the
later version of the methods such as broadcast_append_later_to.
# enqueues a `Turbo::Streams::ActionBroadcastJob`
Turbo::StreamsChannel.broadcast_append_later_to(
@chat_room,
target: "messages_container",
partial: "chat_rooms/message",
locals: { message: message }
)
# enqueues a `Turbo::Streams::BroadcastJob`
Turbo::StreamsChannel.broadcast_render_later_to(
@chat_room,
template: "chat_rooms/update"
)
Check out the source code for all available helpers.
3.2.1. Broadcastable Model Helpers
The turbo-rails gem contains a
Broadcastable
concern which is included in ActiveRecord::Base. It applies Rails conventions
to succinctly broadcast model-specific Turbo Streams. Some example use cases are:
@product = Product.first
# These helpers implicitly broadcast Stream actions to
# the model object's stream.
#
# <%= turbo_stream_from @product %>
# Broadcasts an `append` action containing the partial
# `products/product` targeted at the DOM element with the id `"products"`.
# Conventionally, the target is the pluralized model name, and the
# partial is obtained by calling `@product.to_partial_path`.
@product.broadcast_append
@product.broadcast_append_later
# The `update` action targets the specific model's HTML element.
# In this case, it will target `"product_1"`, where `1` is the ID of
# the `@product` record. The content will be the partial `products/product`.
# The target is generated using `ActionView::RecordIdentifier.dom_id(@product)`,
# and the partial is obtained by calling `@product.to_partial_path`.
@product.broadcast_update
@product.broadcast_update_later
# The `remove` action targets the specific model's HTML element.
# In this case, it will target `product_1`, where `1` is the ID of
# the `@product` record.
@product.broadcast_remove
@product.broadcast_remove_later
# The partial and target can be explicitly defined if required.
@product.broadcast_append(target: "products", partial: "products/product", locals: { product: @product })
@product.broadcast_append_later(target: "products", partial: "products/product", locals: { product: @product })
# Broadcast to a specific stream
@product.broadcast_append_to("product_feed")
@product.broadcast_append_later_to("product_feed")
These Turbo Stream helpers provided by Broadcastable are most useful in lifecycle
callbacks — for example, to append a product to the page whenever a new record
is created:
class Product < ApplicationRecord
after_create_commit -> { broadcast_append_later_to("products") }
end
3.2.1.1. broadcasts_to
The broadcasts_to method configures a model to emit Turbo Streams on creation,
update, and deletion to the supplied stream name (provided via a block or method
signature).
class Product < ApplicationRecord
broadcasts_to ->(product) { "#{product.model_name}_stream" }
end
class Product < ApplicationRecord
broadcasts_to :stream_name
def stream_name
"product_stream"
end
end
The above snippets are equivalent to:
class Product < ApplicationRecord
after_create_commit -> {
broadcast_append_later_to("product_stream", target: "products", partial: "products/product")
}
after_update_commit -> {
broadcast_replace_later_to("product_stream", target: ActionView::RecordIdentifier.dom_id(self), partial: "products/product")
}
after_destroy_commit -> {
broadcast_remove_to("product_stream", target: ActionView::RecordIdentifier.dom_id(self))
}
end
You can customize the partial or template rendered in the broadcast using:
class Product < ApplicationRecord
# Renders `products/_tile.html.erb`
broadcasts_to -> { "product_stream" }, partial: "products/tile"
# Renders `products/update.html.erb`
broadcasts_to -> { "product_stream" }, template: "products/update"
# Renders `products/update.turbo_stream.erb`
broadcasts_to -> { "product_stream" }, template: "products/update", formats: :turbo_stream
end
3.2.1.2. broadcasts
The broadcasts directive emits Turbo Streams to an inferred stream
name (the pluralized model name for create, and the model
instance's stream for update and remove):
class Product < ApplicationRecord
broadcasts
end
This can be expanded as:
class Product < ApplicationRecord
after_create_commit -> {
broadcast_append_later_to("products", target: "products", partial: "products/product")
}
after_update_commit -> {
broadcast_replace_later_to(self, target: ActionView::RecordIdentifier.dom_id(self), partial: "products/product")
}
after_destroy_commit -> {
broadcast_remove_to(self, target: ActionView::RecordIdentifier.dom_id(self))
}
end
Customize the rendered partial or template using:
class Product < ApplicationRecord
# Renders `products/_tile.html.erb`
broadcasts partial: "products/tile"
# Renders `products/update.html.erb`
broadcasts template: "products/update"
# Renders `products/update.turbo_stream.erb`
broadcasts template: "products/update", formats: :turbo_stream
end
When using morphing page refreshes, the broadcasts_refreshes declaration
will trigger Turbo Streams with the action refresh whenever a model changes:
class Product < ApplicationRecord
broadcasts_refreshes
end
This is equivalent to:
class Product < ApplicationRecord
after_create_commit -> { broadcast_refresh_later_to("products") }
after_update_commit -> { broadcast_refresh_later_to(dom_id(self)) }
after_destroy_commit -> { broadcast_refresh_to(dom_id(self)) }
end
You can specify a stream name using broadcasts_refreshes_to:
class Product < ApplicationRecord
broadcasts_refreshes_to :stream_name
def stream_name
"products"
end
end
See the API documentation for all available helpers.
4. Stimulus
Stimulus is a lightweight library to manipulate HTML with reusable pieces of JavaScript logic encapsulated in a JavaScript object, called a controller.
Stimulus has an HTML-centric way of writing JavaScript. The markup is connected to the
controller using a range of data- attributes.
Here's an example of a Stimulus controller:
// hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "name", "output" ]
greet() {
this.outputTarget.textContent =
`Hello, ${this.nameTarget.value}!`
}
}
The above controller uses targets, which are named references to elements
in its HTML scope, to grab an input's value and display a greeting. The
greet() action reads the name target and writes it into the
output target.
It can be attached to the DOM via the data-controller attribute:
<div data-controller="hello">
<input data-hello-target="name" type="text">
<button data-action="click->hello#greet">
Greet
</button>
<span data-hello-target="output">
</span>
</div>
Refer to the Stimulus handbook and reference for complete usage information.
4.1. Creating Controllers
The stimulus-rails gem provides a generator to create Stimulus controllers:
$ bin/rails generate stimulus hello
# Generates app/javascript/controllers/hello_controller.js
This generates the skeleton of a controller:
// app/javascript/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="hello"
export default class extends Controller {
connect() {
}
}
When using an import map to deliver your JavaScript, Rails will automatically eager load all your Stimulus controllers:
// app/javascript/controllers/index.js
// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)
In other JavaScript setups, controllers are loaded manually:
// app/javascript/controllers/index.js
import { application } from "./application"
import HelloController from "./hello_controller"
application.register("hello", HelloController)
This manifest is automatically updated when you create a controller using
bin/rails generate stimulus. Force an update by running:
$ bin/rails stimulus:manifest:update
This might be useful after renaming a controller.
4.2. Using Controllers
In Rails, use Stimulus controllers for client-side use cases where Turbo doesn't apply. It's best used to modify HTML you already have.
In this section we'll discuss an example use case: implementing a button which allows a user to dynamically add text fields to a Rails form.
Consider the below form to create a post:
<%= form_with(model: @post) do |form| %>
<fieldset>
<%= form.label :title %>
<%= form.text_field :title %>
</fieldset>
<fieldset>
<%= form.label :body %>
<%= form.text_area :body %>
</fieldset>
<%= form.submit %>
<% end %>
We'd like to allow the user to create one or more tags to associate with the post. This requires an Add Tag button which dynamically adds a text field to the form where the user can define the tag. This is a perfect use case for Stimulus.
First, ensure the model and controller can accept nested attributes for the tag:
class Post < ApplicationRecord
has_many :tags
accepts_nested_attributes_for :tags, allow_destroy: true, limit: 5
end
class PostsController < ApplicationController
def create
@post = Post.new(post_params)
# ...
end
private
def post_params
params.expect(post: [:title, :body, tags_attributes: [:content, :_destroy]])
end
end
class Tag < ApplicationRecord
belongs_to :post
end
Then, we need to add a <template> element to the form which the
Stimulus controller will use to add the text field:
<%= form_with(model: @post) do |form| %>
<%# ... %>
<template>
<%= form.fields_for :tags, @post.tags.build, child_index: "NEW_RECORD" do |tag_fields| %>
<fieldset data-new-record="true">
<%= tag_fields.text_field :content %>
<%= tag_fields.hidden_field :_destroy %>
<%= tag_fields.button type: :button do %>
Remove
<% end %>
</fieldset>
<% end %>
</template>
<%= form.submit %>
<% end %>
Specifying the NEW_RECORD placeholder as the child_index allows us to replace
it in the Stimulus controller to prevent clashes in elements' id and name
attributes.
Next, generate and implement a Stimulus controller to read this <template> and
add it to the form:
$ bin/rails generate stimulus nested_form
// app/javascript/controllers/nested_form_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="nested-form"
export default class extends Controller {
static targets = ["container", "template"]
add(event) {
event.preventDefault()
// Replace `NEW_RECORD` with the timestamp to ensure each new element is unique
let timestamp = new Date().getTime().toString()
let content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, timestamp)
this.containerTarget.insertAdjacentHTML("beforeend", content)
}
remove(event) {
event.preventDefault()
let wrapper = event.target.closest("fieldset")
// For new `Tag` records, we can simply remove the HTML element
if (wrapper.dataset.newRecord === "true") {
wrapper.remove()
// When a `Tag` record exists, mark it for deletion on the server
// and hide it in the UI.
} else {
wrapper.style.display = "none"
let input = wrapper.querySelector("input[name*='_destroy']")
input.value = "1"
}
}
}
The final step is to wire up the Stimulus controller in the DOM:
<%= form_with(model: @post, data: { controller: "nested-form" }) do |form| %>
<%# ... %>
<%# The container to which the text fields will be added %>
<div data-nested-form-target="container">
</div>
<button type="button" data-action="nested-form#add">
Add tag
</button>
<template data-nested-form-target="template">
<%= form.fields_for :tags, @post.tags.build, child_index: "NEW_RECORD" do |tag| %>
<fieldset data-new-record="true">
<%= tag.text_field :content %>
<%= tag.hidden_field :_destroy %>
<%= tag.button type: :button, data: { action: "nested-form#remove" } do %>
Remove
<% end %>
</fieldset>
<% end %>
</template>
<%= form.submit %>
<% end %>
The user can now dynamically add tags to their post which will be processed on the server.
This is just one example where Stimulus fits the bill. Use it wherever you need to manipulate HTML in your web page. Some more example use cases are:
- Keyboard hotkeys which trigger actions in your app.
- Resize an HTML textarea automatically as a user types.
- A drag and drop interface to upload files.