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:
ruby
#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.
coffeescript
# 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.
```html # app/views/users/_form.html.erb <%= form_with(model: user, id: “new_user”) do |form| %> … <% end %> # =>
```
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.
ruby
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.
ruby
# 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.
coffeescript
# 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.
```coffeescript # 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.
coffeescript
# 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:
js
// 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:
js
// app/views/users/create.js.erb
document.getElementById("sjr").innerHTML = "<%= @user.name %>";
Render partials:
js
// 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
:
```html
```
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:
javascript
"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:
javascript
"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.
ruby
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.
coffeescript
# 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.