Boring Rails

The most underrated Rails helper: dom_id

Jun 28th, 2022 6 min read

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.

From the docs:

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:

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>

Was this article valuable? Subscribe to the low-volume, high-signal newsletter. No spam. All killer, no filler.