Ajax on Rails with Unobtrusive JavaScript

  

AJAX on Rails | Engine Yard

This blog post is for Rails developers that want to use Ajax in their application using Unobtrusive JavaScript. This is divided into these parts:

Introduction
Create a new Rails app
Add a remote form
Tutorial#create action
Bind on ajax: success
Effects
Delete using Ajax
Conclusion

Note: Rails has support for using Javascript frameworks like Vue, React, and StimulusJS through Webpacker. At some point, a framework might be more suitable for your application. Use this blog post to compare Rails UJS and frameworks. In the future, I will write about these frameworks and how to use them with Rails.

Introduction

JavaScript libraries like jQuery makes it easier to make Ajax requests. Rails takes that a step further and you don't even have to make the Ajax request at all. Previously, you need to add remote: true to your forms. form_with was introduced on Rails 5.1 that by default uses Ajax. You have to add local: true if you don't want to use Ajax.

This is a blog post on using Ajax for forms and links with jQuery. I have also blogged about jQuery and Rails in the past. Check this post for a basic introduction to jQuery and Rails.

Even though you can use jQuery to make an Ajax request, we'll use the built-in Rails features to make the Ajax requests for us. This is done through a technique called Unobtrusive JavaScript. The HTML forms and links that Rails helpers generate contain data-remote=true. Rails will look for elements marked as remote and make the Ajax requests when the form is submitted or the link is clicked.

Rails 5.1 removed jQuery for the UJS feature (Unobtrusive JavaScript) and uses its own rails-ujs code. In this post, we will remove rails-ujs and use the jquery-rails gem. The only reason for this is I want to use some jQuery features that's not related to UJS. In a future post, I'll use the default rails-ujs.

Create a new Rails app

This section is a step by step guide to creating a new Rails app. If you just want to read the code, you can skip this section and go to the next one.

Generate a new Rails 5.1 application named tutorial app. This is a simple app that displays a list of tutorials saved in the database.

rails new tutorialapp --skip-turbolinks

Starting Rails 5.1, jQuery is not included by default. Let's add it to the Gemfile.

gem 'jquery-rails'

The jquery-rails gem contains the jQuery files. Install the gem with

cd tutorialapp
bundle install

To include the jQuery files in our application, we will require a few files on app/assets/javascripts/application.js. We'll also remove rails-ujs since we're using jquery_ujs for the same purpose.

//= require jquery3
//= require jquery_ujs
//= require_tree .

Now, let's generate the tutorial resource which will create the Tutorial model and controller and update the routes. The model will have a title and a url, both Strings.

rails g resource tutorial title:string url:string

On app/controllers/tutorials_controller.rb, add the index action.

def index
  @tutorials = Tutorial.all
end

On the index view app/views/tutorials/index.html.erb, display all the tutorials.

<h1>Tutorials</h1>

<ul id=tutorials>
  <% @tutorials.each do |tutorial| %>
    <li><%= tutorial.title %></li>
  <% end %>
</ul>

Before we start the application, let's add a few records so our app has data to display. Add the following to db/seeds.rb.

Tutorial.create(title: 'Encrypted Rails Secrets on Rails 5.1', url: 'https://www.engineyard.com/blog/encrypted-rails-secrets-on-rails-5.1')
Tutorial.create(title: 'Using Docker for Rails', url: 'https://www.engineyard.com/blog/using-docker-for-rails')
Tutorial.create(title: 'Running a Rails App in Kubernetes', url: 'https://www.engineyard.com/blog/kubernetes-tutorial-running-a-rails-app-in-kubernetes')

Then run the migration and seed commands.

bin/rake db:migrate
bin/rake db:seed

Start the application and go to the tutorials index page.

bin/rails server

On your browser, go to http://localhost:3000/tutorials

Add a remote form

Instead of creating a new page with a form, we'll add the form to the existing index page. Our goal is for the page to NOT reload when you click Submit. An Ajax request will be made, the tutorial will be saved to the database, and the new tutorial will appear on the page. This will be faster and give a better user experience than reloading the page.

Let's edit app/views/tutorials/index.html.erb that we created earlier.

<h1>Tutorials</h1>

<ul id=tutorials>
  <% @tutorials.each do |tutorial| %>
    <li><%= tutorial.title %></li>
  <% end %>
</ul>

<%= form_with model: Tutorial.new, data: {'js-tutorial-form' => true} do |form| %>
  Title <%= form.text_field :title %><br>
  URL <%= form.text_field :url %><br>
  <%= form.submit %>
<% end %>

form_with by default creates a remote form. We also added the data attribute js-tutorial-form. The generated HTML is

<form data-js-tutorial-form="true" action="/tutorials" accept-charset="UTF-8" data-remote="true" method="post">

data-remote is used by Rails, specifically jquery_ujs in our app, to make an Ajax request. data-js-tutorial-form is a custom data attribute that we will use later.

Tutorial#create action

The create action on the Tutorial controller saves the tutorial record to the database. After that, on non-Ajax requests, you would usually redirect to the tutorial page or the index page. But here, we'll render a partial for the tutorial.

  def create
    @tutorial = Tutorial.new(params[:tutorial].permit(:title, :url))
    if @tutorial.save
      render partial: "tutorial", locals: {tutorial: @tutorial}
    end
  end

The partial app/views/tutorials/_tutorial.html.erb displays the title of the tutorial inside <li></li>.

<li><%= tutorial.title %></li>

Bind on ajax:success

The tutorial partial isn't automatically displayed on the page. When the Ajax request is successful, Rails fires the ajax:success event. We need JavaScript code to bind to this event.

On application.js, add

$(document).ready(function() {
  $('[data-js-tutorial-form]').on("ajax:success", function(event, data, status, xhr){
    $('#tutorials').append(xhr.responseText);
  });
});

We put our code inside $(document).ready() to make sure that the DOM (Document Object Model) is ready.

We use $('[data-js-tutorial-form]') to get the form then call on with ajax:success and a function.

This function has 4 arguments. Since we bind on the ajax:success event, we know that the request was successful. In this case, we only use xhr.responseText which is the part that we rendered from the create action.

$('#tutorials') is the ul element that displays all our tutorials. We append the new tutorial.

Effects

The code above appends the new tutorial but it can be hard to notice. Let's add the fadeIn effect from jQuery.

$(document).ready(function() {
  $('[data-js-tutorial-form]').on("ajax:success", function(event, data, status, xhr){
    var tutorial = $(xhr.responseText).hide();
    $('#tutorials').append(tutorial);
    tutorial.fadeIn(2000);
  });
});

We hide the new tutorial first, append it, then call fadeIn with an argument of 2000 milliseconds.

There are other effects that you can use like a highlight but that requires jQuery UI.

Delete using Ajax

Let's add a delete link that will delete the tutorial from the database. We'll add the link to the partial and we'll rewrite the index page to use the partial as well.

On app/views/tutorials/index.html.erb, change the ul portion to

<ul id=tutorials>
  <%= render @tutorials %>
</ul

On app/views/tutorials/_tutorial.html.erb, add the delete link. We'll also add a data attribute to the li element.

<li data-js-tutorial-id=<%= tutorial.id %>>
  <%= tutorial.title %> <%= link_to "Delete", tutorial, remote: true, method: :delete, data: { confirm: "Are you sure you want to delete '#{tutorial.title}'?" } %>
</li>

On app/controllers/tutorials_controllers.rb, add the destroy action.

  def destroy
    @tutorial = Tutorial.find(params[:id])
    @tutorial.destroy
    render json: @tutorial
  end

On app/assets/javascripts/application.js, bind to ajax:success.

  $('[data-js-tutorial-id]').on("ajax:success", function(event, data, status, xhr){
    var tutorial_id = xhr.responseJSON.id;
    $('[data-js-tutorial-id=' + tutorial_id + ']').hide();
  });

Each tutorial has a data-js-tutorial-id attribute so $('[data-js-tutorial-id]') is used to bind to the ajax:success event.

xhr.responseJSON returns a json of the tutorial which was generated by the destroy action.

$('[data-js-tutorial-id=' + tutorial_id + ']').hide() hides the tutorial.

Conclusion

jquery-ujs and Rails UJS, in general, makes it easy to use Ajax on your Rails application. The use of data attributes allows you to put all your JavaScript code on the assets directory. You can use IDs and classes but it is not clear which classes are used for styles and which ones are used by JavaScript.

Do you use Rails UJS or are you using a JavaScript framework? We like to hear from you in the comments.

Free Ebook: PaaS Is Dead

Platform as a Service (PaaS) is experiencing a digital transformation, and despite what some may argue, it’s far from dead. Learn why PaaS continues to prove it has a promising future for DevOps.

Download Ebook
PaaS Is Dead

Related posts

Ruby on Rails vs PHP

May 16, 2018

There’s more than one way to build a web application. No matter what type of application you are

Read More

What to Look for When Considering Application Hosting

May 8, 2018

Cloud computing has made hosting business-critical applications easier and less expensive.

Read More

5 Commercial Use Cases Continue to Prove the Value of Ruby on Rails

April 11, 2018

Ruby on Rails continues to gain popularity as an effective platform for developing web and

Read More

Christopher Rigor

 
Christopher Rigor is a Senior Technical Evangelist at Engine Yard. He’s a long time Rails user, system administrator, and recently became a contributor of RailsInstaller. Previously, he was the DevOps Support Manager for Asia-Pacific at Engine Yard.
Find me on:

Comments

Subscribe Here!