A definitive guide to Rails’ unobtrusive JavaScript adapter

Patrik Bóna
Patrik on Rails
Published in
7 min readFeb 10, 2018
Image from pixabay.com.

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:

I’ve added a two second delay to the create action.

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 into data-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:

  1. rails-ujs binds to the click event on links with data-method.
  2. 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 receive xhr and options 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 in ajax:beforeSend. Callbacks receive the xhr object. This event is used internally by rails-ujs to disable clicked remote elements.
  • ajax:stopped — is fired when ajax:before or ajax: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!

--

--