This post is part of Hotwire Summer: a new season of content on Boring Rails!
The dom_id
helper in Rails is over a decade old, but has proven to be an invaluable concept in Hotwire.
This secret workhorse powers all kinds of HTML-related behavior in Rails. It has one key job: making it easy to associate application data with DOM elements.
dom_id
takes two arguments: a record and an optional prefix.
The record
can be anything that responds to to_key
and model_name
, but 99% of the time you are passing it an ActiveRecord model. The prefix
can be anything that responds to to_s
, but 99% of the time it is a symbol.
dom_id(Post.find(45)) # => "post_45"
dom_id(Post.new) # => "new_post"
dom_id(Post.find(45), :edit) # => "edit_post_45"
dom_id(Post.new, :custom) # => "custom_post"
The reason this helper is so nice is that it sets a convention. Instead of defining your own way of identifying markup and later hoping you remember the pattern, Rails provides a standard; you don’t need to switch contexts to know if you set an id to “post_23_comments” or “comments-post-23” or wait, was it “post_23-comments”?
By providing stable id values for your elements, you avoid fragile code like targeting the first element child or finding a node based on the text value.
Here are places where you should regularly reach for dom_id
when building Hotwire apps.
Super clean tag builders
Leaning into HTML markup as the source of truth means adding extra attributes and conditionals when rendering templates. While you can do plain old string interpolation in your ERB templates, Rails has a set of nice tag builders that help you avoid drowning in a sea of brackets, braces, and octothorpes.
<%= tag.div id: dom_id(@post, :comments), class: "flex flex-col divide-y" do %>
<%= render @post.comments %>
<% end %>
As you use these helpers more, make sure you check out these tips:
- Set up the Tailwind Intellisense plugin for VS Code to autocomplete when using tag helpers
- Take advantage of class_name helper (from the
classNames
React API) for conditional classes - For commmon elements, consider extracting a component with a lightweight helper or a library like ViewComponent
Deep linking anchor tags
Since Rails leans so heavily on native browser features, make sure you take advantage of anchor tags on links. You can use dom_id
to scroll the browser directly to an element with the corresponding id
(or generate a permalink that users can share).
<%= link_to "View comment", posts_path(@post, anchor: dom_id(@comment)) %>
You can also use this pattern for redirects. For example, after creating an item in a list, you can redirect back to the index page but scroll to the new item.
class CommentsController < ApplicationController
def create
@post.comments.create!(comment_params)
redirect_to posts_path(@post, anchor: dom_id(@comment))
end
end
A few more tips related to deep linking:
- You can use the
:target
pseudo-class to style the element with an ID matching the URL anchor. In Tailwind, simply use the target prefix (e.g.target:bg-yellow-50
to add a subtle yellow background to an element when it matches the URL anchor) - Another handy CSS property is
scroll-margin-top
: the browser will scroll the targeted element all the way to the top of the window. You may want a little extra padding, but only when you scrolled to the element. Don’t add extra margin or padding to your designs or add weird wrapper divs…scroll-margin-top
(Tailwind class:scroll-mt
) is the answer.
With Turbo Frames
When you start adding Turbo Frames to your application, you’ll need to provide an id for the frame tag. The Rails turbo_frame_tag
uses – you guessed it – dom_id
under the hood. But you can also pass in your own ids as well.
turbo_frame_tag @post # => <turbo-frame id="post_123"></turbo-frame>
turbo_frame_tag dom_id(@post, :comments) # => <turbo-frame id="comments_post_123"></turbo-frame>
Since a Turbo Frame needs to be unique per page, dom_id
is a convenient way to generate frame ids, especially if you have multiple frames on the page.
And since you can navigate a frame via other links, it’s a great convention to follow:
<%= turbo_frame_tag @comment, src: comment_path(@comment) %>
<!-- Elsewhere... -->
<%= link_to "Edit", edit_comment_path(@comment), data: { turbo_frame: @comment } %>
Scoping Turbo Stream responses
Making small mutations on a page with Turbo Streams is super powerful, but since your stream actions are in a separate turbo_stream.erb
file, it can be tricky to match up your ids between views.
Once again, dom_id
keeps things consistent.
<!-- app/views/plans/quick_edit/update.turbo_stream.erb -->
<%= turbo_stream.replace dom_id(@plan, :title), partial: "plans/title" %>
<%= turbo_stream.replace dom_id(@plan, :notes), partial: "plans/notes" %>
<%= turbo_stream.replace dom_id(@plan, :assigned), partial: "plans/assigned" %>
And by adding the correct ids to your markup, the Turbo Stream responses are super clean:
<!-- app/views/comments/destroy.turbo_stream.erb -->
<!-- Calls `dom_id(@comment)` under the hood -->
<%= turbo_stream.remove @comment %>
Wrap it up
Who would have thought that a simple helper to generate HTML id
values from your application models would be such a useful concept that, more than a decade after first being introduced, it continues to prove helpful even on the newest and shiniest parts of Rails.
If you haven’t been using dom_id
before, consider it the next time you write a view in your Rails app. You might be surprised at how much more pleasant it is to create HTML when you aren’t littering it with code like:
<div id='<%= "#{@post.id}-comments" %>'>
</div>
Top comments (0)