DEV Community

Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

Rails dom_id helper without exposing the primary id

This article was originally published on Rails Designer


Rails' dom_id is a useful little helper especially in Rails apps with Turbo. I like to use it as it provides a consistent output for your id-attributes. Not having to think about (and mixing it up) how to structure even an id-attribute is just one of those things I enjoy about Rails.

This is how it is used:

<%= turbo_frame_tag dom_id(message, :votes) do %>
  <%= button_to "👍", votes_path, params: {message_id: message, vote: "up"} %>

  <%= button_to "👎", votes_path, params: {message_id: message, vote: "down"} %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

As a small aside: turbo_frame_tag's first arguments takes any representation of a string as an array as the id. So above cóuld be rewritten as turbo_frame_tag message, :votes

And that would both render:

<turbo-frame id="votes_message_1">
  Rest of the HTML here
</turbo-frame>
Enter fullscreen mode Exit fullscreen mode

Other ways to use the dom_id helper is:

  • dom_id(Message.find(42)); this would output message_42;
  • dom_id(Message); this would output message;

It looks for an id on the passed object (and return _new if none is found). The id is, by default, the primary_key which value is increased incrementally. This might be for business- and security reasons, not what you want.

Now previously my solution was to use the stealth_dom_id gem I released late last year. Recently someone pointed out the issue with it using TurboStream Broadcasts where it would still use the record's id. The turbo-rails gem uses ActionView::RecordIdentifier under the hood; nót the dom_id view helper. Makes sense.

So after some investigation on the internals I have decided to sunset the stealth_dom_id. My suggestion now is to define the to_key method on the model.

I use it in an updated version of my sluggable concern (lib/sluggable.rb) that I copy over to every app I build myself (and for others). The basics look like this:

module Sluggable
  extend ActiveSupport::Concern

  included do
    before_create :set_slug
  end

  def to_param = slug

  private

  def set_slug
    return if slug

    slug = nil

    loop do
      slug = SecureRandom.hex(4)

      break unless self.class.name.constantize.where(slug: slug).exists?
    end

    self.slug = slug
  end
end
Enter fullscreen mode Exit fullscreen mode

This concern assumes the model has a slug column. It is then used as User.find_by(slug: params[:id]).

I've now updated this concern to include the to_key method.

module Sluggable
  extend ActiveSupport::Concern

    included do
    before_create :set_slug
  end

+  def to_key = [slug]
+
   def to_param = slug
end
Enter fullscreen mode Exit fullscreen mode

Now when you use dom_id (or the shorthand) version in the turbo_frame_tag (or anywhere in your HTML or TurboStream Broadcasts) it uses the slug value instead of the id:

  • turbo_frame_tag dom_id(message, :votes) outputs <turbo-frame id="votes_message_a1b2c3">;
  • turbo_frame_tag message, :votes outputs <turbo-frame id="votes_message_a1b2c3">

Top comments (2)

Collapse
 
epigene profile image
Augusts Bautra

Obfuscating DB ids is an important topic, thanks for bringing it up!

I'd like to point out what I believe is a big flaw with the slug generation - depending on the use-case and number of records (SecureRandom.hex(4) can produce up to 4,294,967,296 unique values), we can run into collisions and excessive looping which will impact performance.

Perhaps a better solution would:

  1. Allow full a-z (or even a-zA-Z), not just hex's [a-f].
  2. Be deterministic based on the ID passed (plus some secret salt)

I hear github.com/peterhellberg/hashids.rb is on the right path.

Collapse
 
railsdesigner profile image
Rails Designer

Yes, while not the topic of the post, that is a good thing to point out. 👍