A definitive guide to Rails’ unobtrusive JavaScript adapter
This article is about rails-ujs, a JavaScript library bundled with Rails. It eliminates double-click issues, provides easy to use confirmation boxes, allow non-GET requests from HTML links and last but not least it provides seamless Ajax integration with Rails.
Double-click issues
People tend to double-click on links and buttons. If a user double-clicks on a submit button, then the related form gets submitted twice. Have you ever seen duplicate comments on the web? They were probably caused by unhandled double-clicks.
Fortunately, rails-ujs got us covered. We don’t have to deal with double-clicks on the server-side. It is easier to disable links and form buttons after the first click. We can achieve this with the data-disable
HTML attribute:
<input type="submit" value="Create User" data-disable="true">
It is even simpler with view helpers:
<%= submit_tag "Create User", data: { disable: true } %>
This disables the button after the first click and ignores all other clicks. You can see it in the following example:
We can even change the button text after the first click with data-disable-with
:
<%= submit_tag "Create User", data: { disable_with: "Creating user..." } %>
data: { disable_with: "Creating user..." }
gets translated intodata-disable-with="Creating user..."
in the generated HTML code.
Double-click protection looks like a simple thing to implement, but it is not. You have to make sure that all elements are re-enabled after XHR calls (even failed ones), or that they are not disabled at all when a user opens a link in a new tab with Ctrl + click.
Or, you can just use data-disable
or data-disable-with
and do not worry about double-clicks anymore.
Confirmation boxes
If you have a link, button or a form with a potentially destructive action, you might want to ask your users for a confirmation before you process a request created by such element.
This is again, really easy:
<input type="submit" value="Delete User" data-confirm="Are you sure?">
Or with a view helper:
<%= submit_tag "Delete User", data: { confirm: "Are you sure" } %>
This shows a standard JavaScript confirmation box after the click:
Make non-GET requests from links
Let’s say you want to delete a user. You need to send a DELETE request in order to do that, but HTML links can send only GET requests.
Don’t worry, rails-ujs got you covered. You can send any type of request with data-method
:
<a href="/users/1" data-method="delete">Delete user</a>
Or with a helper:
<%= link_to "Delete user", user_path(user), data: { method: "delete" } %>
Also, this plays nicely with data-confirm
:
<%= link_to "Delete user", user_path(user), data: { method: "delete", confirm: "Are you sure?" } %>
You can set HTTP method for links and other elements which can create XHR requests (see later).
Anyway, I said that links can create only GET requests, so how does data-method
work?
You can see it here. But in short:
- rails-ujs binds to the click event on links with
data-method
. - When a link is clicked, then the default action is stopped and rails-ujs creates a form with action set to
href
from the link, set the requested method on the form and then submits it. It also takes care of CSRF tokens so you don’t need to.
Ajax (XHR) calls
If you want to submit your action via an XHR request instead of a standard request, then you need to add data-remote="true"
to your element. This can be done for a link, a form or, also for input elements (see the Advanced usage section).
Example:
<a href="/users/1" data-method="delete" data-remote="true">Delete user</a>
Or with a view helper:
<%= link_to "Delete user", user_path(user), data: { method: "delete", remote: true } %>
This creates an Ajax link for deleting a user. If you use Turbolinks, then you don’t have to do anything special in your controller and it will just work:
# app/controllers/users_controller.rbclass UserController < ApplicationController
def destroy
User.find(params[:id]).destroy
redirect_to users_path
end
end
When you use redirect_to
then turbolinks-rails generates the following response:
Turbolinks.clearCache()
Turbolinks.visit("http://web.dev/users",{"action":"replace"})
This gets evaluated by the browser and Turbolinks redirects the user to the user index. No full page refresh is needed, everything is done via Ajax and you only needed to set two data attributes on the delete link.
Do you want to learn more about Ajax on Rails? Then check out my previous articles about it. If you want to know even more, then check out my course Ajax on Rails.
Integration with view helpers
XHR forms with form_with
form_with
adds data-remote="true"
by default to all forms. This can be disabled per form with local: true
option, or globally in the app configuration with:
# config/initializers/action_view.rbRails.application.config.action_view.form_with_generates_remote_forms = false
Learn more about
form_with
in Rails 5.1’s form_with vs. form_tag vs. form_for.
Automatic double-click protection for forms
Form submit buttons created with submit_tag
or submit
helpers automatically add data-disable-with
attribute to the generated HTML.
<%= submit_tag "Submit" %>
Generates:
<input type="submit" name="commit" value="Submit" data-disable-with="Submit">
This can be disabled with:
# config/initializers/action_view.rbRails.application.config.action_view.automatically_disable_submit_tag = false
However, I don’t see a good reason to change Rails defaults and disable automatically generated data attributes for rails-ujs.
Advanced usage
We already saw some basic examples, but there is even more what you can do with rails-ujs.
Binding to the change event on form elements
You can combine data-remote
and data-url
on any input
, select
or textarea
element. This will bind to the change event on a given element. When the value of the element is changed an XHR request will be send to the URL specified in the data-url
attribute.
It can be hard to come up with a good use case for this, but I got one. A to-do checkbox:
<%= check_box_tag dom_id(todo, :checkbox), nil, todo.completed?, data: { url: todo_toggles_path(todo), remote: :true, method: :post } %>
This generates a check box which will send an XHR POST request to the server when it’s value get changed.
Set Accept header for XHR requests
data-type
changes the HTTP Accept
header for XHR requests. By default it is set to:
text/javascript, application/javascript, application/ecmascript, application/x-ecmascript
This is great for Server-generated JavaScript Responses because it tells the server that you expect a JavaScript response.
But if you want to receive a JSON response then adddata-type="json"
to your element and your are done. This will set the Accept header to:
application/json, text/javascript
Add additional params to XHR requests
You can add additional params to XHR requests with data-params
. However I am having a hard time creating an useful example for this feature because usually you can pass params via URL, so it doesn’t make much sense adding them to the data-params
attribute.
Anyway, you can add additional params to XHR requests like this:
<%= link_to "Add to cart", cart_path, data: { method: :post, remote: true, params: { product_id: product.id }.to_param } %>
This link will create an XHR POST request with product_id
value in POST data.
Subscribing to Ajax events
When rails-ujs issues an XHR request, then it fires a bunch of different events. You can subscribe to them and stop or modify the XHR request.
ajax:before
— is fired before any XHR request processing. Stopping the event aborts the request.ajax:beforeSend
— is fired before the request is send. Callbacks subscribed to this event receivexhr
andoptions
objects which can be modified before the request is send. Stopping the event aborts the request.ajax:send
— is fired directly before the XHR is send if it wasn’t stopped inajax:beforeSend
. Callbacks receive thexhr
object. This event is used internally by rails-ujs to disable clicked remote elements.ajax:stopped
— is fired whenajax:before
orajax:beforeSend
are stopped. It is also used internally to enable disabled elements.ajax:success
— is fired after XHR request completion if the HTTP response was success.ajax:error
— is fired if the server returned an error.ajax:complete
— is fire when the request has been completed.
Conclusion
That’s it! rails-ujs is a simple but powerful tool which integrates nicely with Rails. I hope that I did not forget anything. If I did, then please let me know and I’ll be happy to add it here.
If you liked this article, then please consider subscribing to my personal newsletter to receive more content from me. Every new subscriber makes me really happy. Thank you!