Non-Unique DOM IDs
Every good web developer knows that IDs on DOM elements should be unique. I say should because browsers faithfully render pages that break this rule. That said, Chrome now shows an error message in the developer console under certain circumstances with version 63 - released earlier this month. Through the new error message, I learned how difficult it can be to follow the unique ID rule when using my beloved Ruby on Rails.
The New Error
One of my favorite features of Chrome has always been the developer tools. I consistently have the console open when working on my web projects. Earlier this month, I was greeted with a new error message on a page of our app that I am on all the time. The message looked similar to the following:
[DOM] Found 2 elements with non-unique id #sample_id: (More info: https://goo.gl/9p2vKq)
The error is important because any javascript that attempts to manipulate the
second element with that ID will be messing with the wrong element. The jQuery
id selector $("#...")
and the regular document.getElementById("...")
method
returns the first element in the DOM with the given ID. A mistake here can
lead to some difficult to find bugs. While there are ways to get multiple
elements with the same ID (e.g. document.querySelectorAll('[id=test_id]')
they’re not common and I don’t recommend using them unless you have a really
good reason to do so.
Further investigation into the Chrome provided error, reveals that it is only given in a very special set of circumstances. The following HTML does not produce the error.
<h1 id="test">Duplicate ID Challenge</h1>
<p id="test">Some text</p>
<input id="test" type="text">
<input id="test">
However, set one of the input
fields to type="password"
and the error is
triggered. I find it amusing that the message still reads “Found 2 elements”
despite the fact that there are 4 with the same ID. It turns out that Chrome is
only counting input
elements that contain the same ID where at least one of
them is of type password
.
The link provided in the error message leads to a page that highlights the use of password forms by browsers and password managers. These tools provide auto-complete functionality for their users. This suggests the reason for the narrow focus lies in the importance of autocompleted password fields, which makes sense. The bottom of the page contains the reminder to “Follow HTML guidelines” with regard to unique IDs.
The Rails Challenge
Ruby on Rails provides a number of helpers for developers such as myself. It’s part of the reason I love Rails so much. Unfortunately, it goes a little too far with the magic from time to time. This is one such occasion in my opinion.
Several Rails view helpers automatically put IDs on elements that are not explicitly set. Here are a few examples:
<%= form_for :test_model do |f| %>
<%= f.hidden_field :command %>
<% end %>
yields
<form action="/some/url" accept-charset="UTF-8" method="post">
<input type="hidden" name="test_model[command]" id="test_model_command">
</form>
Another example:
<%= test_field :test_model, :command %>
gives
<input name="test_model[command]" id="test_model_command">
One more:
<%= form_tag("/some/url", method: "get") do %>
<%= label_tag(:test_model_command, "Label:") %>
<%= text_field_tag(:test_model_command) %>
<%= submit_tag("Submit") %>
<% end %>
turns into
<form action="/some/url" accept-charset="UTF-8" method="get">
<label for="test_model_command">Label:</label>
<input type="text" name="test_model_command" id="test_model_command">
<input type="submit" name="commit" value="Submit" data-disable-with="Submit">
</form>
Notice that all three of the above examples are different methods to create an input field but all 3 generate the same ubiquitous ID. The result can (and did) lead to duplicate IDs that no one on our team caught. We even had javascript that used the ID as a selector and got lucky it didn’t cause us any known trouble.
The Takeaway
Previously, anytime I put an ID on an element or used an ID as a selector, I would search the project for the string of text I intended to use as the ID. From now on, I’ll be taking this practice one step further. I will render the page the ID is being added to in the browser, then search the DOM within the developer tools for the ID. This will catch any Rails generated IDs as well as explicitly set IDs we’ve created.
Final Thoughts
Even though the duplicate ID hadn’t caused us any known grief, I was happy to see the new error in Chrome. I am disappointed to find out that it is only shown under a specific set of circumstances. I agree that password fields are especially important, but it would be nice to be alerted to duplicate IDs on any page.
I’m not sure what the purpose is for Rails to add IDs to elements for me. I haven’t noticed any strange behavior when placing my own ID in the view helpers (which I always do when I need one). With this understanding, I would prefer to not have the “help” and just leave the ID off the generated elements. If you have a good reason to validate Rails behavior, please let me know.
Oh, The Fix
Update June 23, 2018
I want to thank an anonymous reader for pointing out the lack of solution to the above problem. To correct any duplicate IDs that may be generated you need to provide unique IDs yourself. Each element generator in Rails provides a way to do this called an “options hash”. In the first example, add your unique ID like this:
<%= form_for :test_model do |f| %>
<%= f.hidden_field :command, id: 'some-unique-id' %>
<% end %>
The final argument given to f.hidden_field
is the options hash (remember that
in Ruby, when the final argument to a method is a hash, you can omit the {
braces }). It passes the symbol key id
along with the value
some-unique-id
. Similarly, the second example would look like this:
<%= text_field :test_model, :command, id: 'another-unique-id' %>
Add your own ID to the third example like so:
<%= form_tag("/some/url", method: "get") do %>
<%= label_tag(:test_model_command, "Label:") %>
<%= text_field_tag(:test_model_command, nil, id: 'this-unique-id') %>
<%= submit_tag("Submit") %>
<% end %>
Note the nil
in the example above is the value
of the text_field_tag
(unimportant for the context of this article but included for completeness).
These additions will stop Rails from adding an auto-generated ID to the
particular field and use your ID instead.
One final example. If you’re generating fields in a loop, use something unique in the loop within the ID as shown in this very contrived example:
<% (1..4).each do |number| %>
<%= text_field :test_model, :command, id: "unique-id-#{number}" %>
<% end %>
Some documentation that describes the options hash but does not talk about ID’s specifically can be found in the Ruby on Rails Guides (note: HTML classes are added the same way as IDs in Rails.)