Blog icon

How to structure JavaScript code when using AJAX in Rails

By Stephen Huang,
Stephen Huang
Scroll down to read

AJAX is a great solution that allows you to send and retrieve data from the server and update the page without having to reload the entire page.

In this article I will show you two strategies in Rails to use the data from server by executing JavaScript code: AJAX Callback Function and Server-generated JavaScript Responses. Then I'll explain how they work, and describe their benefits.

Since 5.1, Rails no longer depends on jQuery. A plain JavaScript implementation called rails-ujs replaces jquery-ujs to take advantage of the Unobtrusive JavaScript (UJS) feature in Rails. Although they are implemented in different ways, their concepts are the same. In this article, I will use plain JavaScript for consistency.

Let's start off by creating a Rails app and set it up to send AJAX requests to the server.

Create a AJAX form

Begin with a simple Rails app with a user object:

$ rails new ajax-demo
$ cd ajax-demo
$ rails g scaffold User name:string
$ rake db:migrate

Then we need to create a simple form for that user:

#app/views/users/_form.html.erb

<%= form_with(model: user, id: "new_user", local: true) do |form| %>
  ...
<% end %>

<%= tag.div id: "ajax" %>
<%= tag.div id: "sjr" %>

We need to add an id attribute to the form to help us locate form element in JavaScript. We also add two div tags with id that will be used to update page later.

Solution I: Intercept the submit event in JavaScript

To create the AJAX form, the first solution is to add an event handler to intercept the submit event on the form. And then inside the handler, we serialise the form data and create an AJAX request.

# app/assets/javascripts/users.coffee

  form = document.getElementById("new_user")
  form.addEventListener "submit", (event) ->
    element = this

    # Create a AJAX request
    Rails.ajax
      type: "POST",
      url: "/users",
      data: new FormData(element),
      dataType: 'script',
      success: (args...) ->
        console.log "Ajax callback function"

    Rails.stopEverything event

Solution II: Remote elements

Rails provides a better way to simply build the AJAX form. Supported by rails-ujs, we can use the remote data tag in the form.

The build-in form_with helper includes the remote data tag by default. We can enable AJAX form by removing local: true option. Then the form will be sent as an AJAX request.

# app/views/users/_form.html.erb

<%= form_with(model: user, id: "new_user") do |form| %>
  ...
<% end %>

# =>
<form action="/users" id="new_user" method="post" data-remote="true">
  ...
</form>

dataType attribute

We specify the dataType to tell the server which format we want it to return. When we use a remote form, it will be set to script by default, which means that the server will return a JS response. If we want to receive a JSON response, we can set the dataType to json. For a remote form, we can provide the type using data-type tag.

Rails.ajax
    ...,
    dataType: 'script',
    ...

# OR
<%= form_with(model: user, id: "new_user", data: { type: "json" }) do |form| %>

The server will return a response based on the dataType. If it's JS and format.js has been enabled, it will render the related js.erb template file.

# app/controllers/users_controller.rb
respond_to do |format|
  if @user.save
    format.html { redirect_to @user, notice: 'User was successfully created.' }
    format.json { render :show, status: :created, location: @user }
    format.js
  else
    format.html { render :new }
    format.json { render json: @user.errors, status: :unprocessable_entity }
    format.js
  end
end

Update the page with JavaScript

After the AJAX request is sent, the server will have the data ready for us to be used on the front end. In our Rails app, there are two strategies for doing this: AJAX Callback Function and Server-generated JavaScript Responses (SJR).

Strategy I: AJAX Callback Function

When we use AJAX and receive the response from server, it will trigger the callback functions, such as success, error, etc, based on the status code. The callback functions are defined when we create the AJAX request. You can put your JavaScript code here and it will be executed when it is fired.

# app/assets/javascripts/users.coffee
Rails.ajax
  ...
  success: () ->
    console.log "Ajax callback function"
    document.getElementById("ajax").innerHTML ="<div>Ajax Success</div>"
  ...

You can also put the JavaScript code in the callback function of the event. Unlike jQuery, the Rails AJAX doesn't fire the event for the callbacks by default. However, if you use the remote data tag, the rails-ujs will fire the event for you.

# https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/features/remote.coffee#L66
ajax(
  ...
  success: (args...) -> fire(element, 'ajax:success', args)
  ...
)

Then we can add the ajax:success listener and put our JavaScript code in the callback function.

# app/assets/javascripts/users.coffee
document.body.addEventListener 'ajax:success', (event) ->
  console.log "Ajax event callback function"
  document.getElementById("ajax").innerHTML ="<div>Ajax Success</div>"

Strategy II: Server-generated JavaScript Responses (SJR)

Rails provides another way to execute the JavaScript code: Server-generated JavaScript Responses (SJR), supported by rails-ujs.

After the server has processed the request, it will generate a JavaScript response by rendering the JS template. If you don't specify a template, it will look for the default template file. In our example it is create.js.erb. This template includes our JavaScript code which will be executed after the response is sent to the client.

The Javascript template looks like this:

// app/views/users/create.js.erb
console.log("SJR template");

The template supports ERB which allows you to insert Ruby code. It means we can directly use instance variables, fetch database, call methods, render partials, etc in the template.

Use instance variables:

// app/views/users/create.js.erb
document.getElementById("sjr").innerHTML = "<%= @user.name %>";

Render partials:

// app/views/users/create.js.erb
// render user/_user.html.erb
document.getElementById("sjr").innerHTML = "<%= j render @user %>";

escape_javascript method

When we render a HTML partial and use it in a JavaScript template, we need to make sure it is in the correct format. For example, we have a partial _user.html.erb:

<!-- app/views/users/_user.html.erb -->
<div id="name">Name: <%= @user.name %></div>

When this template is rendered, it will generate an IO string "<div id=\"name\">Name: Stephen</div>\n" with escape character \ for the special characters. However, when our JS template is rendered, it will also add escape characters. When the client receives the response, it becomes:

"console.log(\"SJR template\");\ndocument.getElementById(\"sjr\").innerHTML = \"<div id=\"name\">Name: Stephen</div>\n\";\n"

After the response is parsed by the client browser, the \n in the HTML string will be treated as a new line in JavaScript, and the double quote in HTML string will be treated as a close quote in JavaScript. This causes the JavaScript code to be incorrectly formatted:

"console.log("SJR template");
document.getElementById("sjr").innerHTML = "<div id="name">Name: Stephen</div>
";
"

Fortunately, Rails provides a function called escape_javascript or j to escape the string for JavaScript responses.

j render @user
  Rendered users/_user.html.erb (0.1ms)
# => "<div id=\\\"name\\\">Name: Stephen<\\/div>\\n"

When the client receives the response, this content will be parsed to "<div id=\"name\">Name: Stephen<\/div>\n" which is a valid javaScript string.

Advanced: How the JavaScript code in SJR executes

When the client receives the JS response from the server, rails-ujs will help us to execute our JavaScript code.

# https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee#L63

processResponse = (response, type) ->
  if typeof response is 'string' and typeof type is 'string'
    if type.match(/\bjson\b/)
      ...
    else if type.match(/\b(?:java|ecma)script\b/)
      script = document.createElement('script')
      script.setAttribute('nonce', cspNonce())
      script.text = response
      document.head.appendChild(script).parentNode.removeChild(script)
    else if type.match(/\bxml\b/)
      ...
  response

From the source code, you can see that our JavaScript code will be added into a new script node. Then the script node will append to the document of the page, which will load immediately. After that, this script node will be removed. That's how the SJR JavaScript code executes.

Conclusion

Both strategies allow you to add your JavaScript code and update the page without a full refresh.

If you only have some simple JavaScript code, you can just put it in the AJAX callback function. It's straightforward and the pure JavaScript way.

However, if your JavaScript code relies on the logic in the server, SJR will be easier to generate the JavaScript code rather than send all the data to client. It simplifies the front-end logic and makes the website faster. Additionally, you can reuse the methods and templates in the Rails framework.

To find out how reinteractive can turn your web application vision into reality, get in touch with us through our contact form or call us on +61 2 8019 7252.