Server generated JavaScript Responses (SJR) in Rails


Asynchronous JavasScript and XML aka AJAX (not the mythical hero) is a set of web development techniques to create asynchronous applications. Using AJAX, clients send and retrieve data from a server asynchronously without causing full-page re-renders or redirection. The decoupling of data retrieval from presentation enables web applications to dynamically change content improving the user's experience (Wikipedia). For example, when a user submits a form request, their entry is appended to the display table automatically instead of redirecting to the index page.

Alongside AJAX in Rails comes Sever generated JavaScript Responses. SJR, according to the Ruby on Rails creator @DHH, is a technique where:

  1. An XMLHttpRequest form is submitted
  2. The server updates a model object
  3. The server generates a JavaScript response that includes the updated HTML template
  4. The client evaluates the JavaScript returned by the server to update the DOM

The result is an update to the HTML DOM that does not cause the full page to reload. Let's look at an example below:

# app/views/notes/index.html.erb
<h2> All Notes </h2>
# Renders the partial notes/_note.html.erb
<div id="notes_list">
  <%= render @notes %>
</div>

<% form_with model: Note.new do |f| %> ❶
 # ... 
 <%= f.submit "Save Note" %>
<% end %>

# app/controllers/notes_controller.rb
class NotesController < ApplicationController
  def create
   @note = Note.create!(params)
   respond_to do |format|
    # When no JavaScript available use HTML
    format.html { redirect_to @note }
    # Renders create.js.rb
    format.js ❷
   end
  end
end

# app/views/notes/create.js.erb
var notes_list = document.querySelector('#notes_list')
// renders the notes/_note.html.erb partials
notes_list.innerHTML += ('<%= j render @note %>') ❸

In this example, we have an index template that contains a form ❶ for creating new notes via AJAX requests. When the form is submitted, the NotesController create action saves the Note to the database and renders the create.js.erb ❷ template in response.  Inside create.js.erb lives JavaScript logic to search the current HTML for an element with the id notes_list ❸, and append to its HTML a rendering of created note ❸ (the "j" function means escape JavaScript).

Because JavaScript generated from the server is sent directly to the client, the browser does not need to re-render the page or redirect anywhere. The browser simply takes the JavaScript response, evaluates it, and renders the new note into the existing HTML according to the steps outlined in the response. Pretty slick huh? This results in an overall "smoother" experience with fewer page flickers and jumping from webpage to webpage. With a bit better understanding of how server-generated JavaScript responses work, let's take a look at implementation in a creation form.

You, Me and SJR

We can use an example of SJR to surface errors and feedback on form submission. Let's take a look at the create action for a controller:

# app/controllers/notes_controller.rb [ref6]
# --- snip --- 
def create
  # --- snip --- 
  respond_to do |format|
   if @note.save
    format.html { redirect_to @note, success: "Note created" } 
   else
    format.html { render :new }
    # Delegates to create.js.erb template
    format.js
   end
  end
end
# --- snip --- 

From the code listing above it is clear that the controller action is capable of responding to JavaScript requests using the format object's js method. However, like all responses, by convention, the default behaviour is to look for a template with the same name as the action. In other words, the controller is looking for a template called create.js.erb to render that does not exist and therefore will currently return no content. We can create this template in the same view/notes directory as our ERB templates.

# app/views/notes/create.js.erb

<% # Server generated JavaScript for AJAX submitted form %>
formElement = document.querySelector("form") 
formElement.innerHTML = "<%= j render "form" %>" ❶

The server-generated JavaScript template we've created does not do anything interesting. The template just re-renders an existing new note form ❶. However, because we have added a template, the server would cease responding with a 204 HTTP code and start responding with a 200.

To display form validation errors on submission we can make use of a  _flash.html.erb partial to render the form's validation errors without redirecting or re-rendering the page. To do this, we will need to make two modifications in create.js.erb and the create action:

# app/views/traders/create.js.erb
<% # Server generated JavaScript for AJAX submitted form %>
flash = document.querySelector('#flash-container')
flash.innerHTML = "<%= j render "shared/flash" %> ❶"

formElement = document.querySelector("form") 
formElement.innerHTML = "<%= j render "form" %>" 

# app/controllers/notes_controller.rb
def create
# --- snip ---
  format.js do
   flash.now[:warning] = @notes.errors.full_messages.first ❷
  end
# --- snip ---
end

In the create.js.erb response we replace the HTML content of the flash-container with our flash partial to render our alerting ❶. Note that we would need to adapt the main application.html.erb to render the shared/flash partial in order for this to work. In other words, we would need to update the application.html.erb code like so:

# app/views/applications.html.erb

<!DOCTYPE html>
<html>
  <head>
    <!-- snipped -->
  </head>

  <body>
     <!-- snipped -->
    <%= render 'shared/flash'%>
    <div class='content' >
      <%= yield %>
    </div>
  </body>
</html>

Now,  in the create action using the flash.now method will display the error message in the current action ❷. It's a bit nuanced but flash and flash.now methods differ slightly in that the former makes the error message available in the next action (e.g. the next redirect or page), whereas, flash.now does so in the current action. Because we are not redirecting to a new action which would cause a full page re-render, we use flash.now to set our warning key.


Once you've added the code above, submitting the form should produce a validation error flash message.