DHH has long promoted the idea of using server generated javascript responses instead of using a client side javascript framework. In this post, I show how to use JS responses to write interactive apps.

I’m a big fan of using the simplest tools possible for a job and when I heard DHH promote the idea of using server generated JS responses as a way to avoid using a client side javascript framework I was intrigued. Was it really possible write the majority of my app’s code in Ruby? I’m not ideologically opposed to using a client side framework. If it’s the right tool for the job, then I won’t hesitate to use one. However, if there’s away for me to reduce my app’s complexity and deliver the features we’ve designed with one toolset, why not do it?!

Overview

To give the idea a thorough exploration, I built a small Rails 4.2 app called People. People1 looks a lot like the todo list apps used to test web frameworks except you manage people instead of todos.

javascript_response_demo

It turns out it is easy. Here’s a gif showing the app in action. I wanted to see how well the approach allows me to:

  • create, edit, delete, and show people
  • show validations and flash messages
  • edit a resource in place

Let’s walk through the process of writing the app.

Create the App

First, I create a new app called “people”. Once Rails does it’s thing, I use the rails generators to scaffold a Person model.

rails new people
cd people
rails g scaffold Person first_name:string last_name:string
rake db:migrate

I don’t need everything rails generates by default, so I delete scaffold.scss.

rm app/assets/stylesheets/scaffolds.scss

To make things look decent without too working hard (We’re prototyping here!), I add gems Bootstrap and Font Awesome to my gemfile. I also remove gem turbolinks because I don’t really need it and I don’t want it causing me any confusion.

gem 'bootstrap-sass', '~> 3.3.4'
gem 'font-awesome-sass', '~> 4.3.0'

bundle install installs my new gems and I’m almost done my setup. I just need to setup Bootstrap and Font Awesome.

// application.js
//= require jquery
//= require jquery_ujs
//= require bootstrap-sprockets
//= require_tree .

Rename application.css to application.scss and write:

// application.scss
@import "bootstrap-sprockets";
@import "bootstrap";
@import "font-awesome-sprockets";
@import "font-awesome";

@import "people";

Finally, I add root 'people#index' to routes.rb, so the app has somewhere to go when it’s opened.

Create the Responses

Now that I have the app setup, I can begin my experiment. I start by building the index page because it’s the main page of the app. Just like the todo list apps, I want a list of people. I want to use a form to add new people to my list, so I create an instance of Person.

# people_controller.rb
def index
    @person = Person.new
    @people = Person.all
end

I render the form on the index page(index.html.erb) and display the list of people:

<h1>People</h1>

<%= render 'people/form' %>

<div class="row">
  <div class="col-sm-6">
    <div class="people">
      <%= render "people/list", people: @people %>
    </div>
  </div>
</div>

_form.html.erb:

<%= form_for(@person, remote: true, html: {class: "form-horizontal"}) do |f| %>
  <div class="form-group">
    <%= f.label :first_name, class: "col-sm-2 control-label" %>
    <div class="col-sm-4">
      <%= f.text_field :first_name, class: "form-control" %>
    </div>
  </div>

  <div class="form-group">
    <%= f.label :last_name, class: "col-sm-2 control-label" %>
    <div class="col-sm-4">
    <%= f.text_field :last_name, class: "form-control" %>
    </div>
  </div>

  <div class="form-group">
   <div class="col-sm-offset-2 col-sm-10">
    <%= f.submit "Add", data:{disable_with: "adding"}, class: "btn btn-primary" %>
    </div>
  </div>
<% end %>

_list.html.erb:

<ul class="list-group">
  <% people.each do |person| %>
    <%= render person %>
  <% end %>
</ul>

_person.htm.erb:

<%= content_tag_for :li, person, data: {remote: true, method: :get}, rel: "no-follow", href: edit_person_path(person), class: "list-group-item" do %>
  <div class="person_fields">
  <%= person.first_name %> <%= person.last_name %>
  </div>
  <%= link_to person, remote: true, method: :delete, data: { confirm: 'Are you sure?' } do %>
    <%= icon('trash')%>
  <% end %>
<% end %>

In _form.html.erb, I set the form to submit using AJAX by giving the option remote: true. This allows me to submit the form without leaving the page. To handle this remote request, I need to modify people_controller.rb to return javascript. Because I don’t want to support any other type of request, I remove format.html and format.json. In case I want to render a flash message, I set it using flash.now, which makes the message available in the current request. I want to update the list of people and want the list to be sorted, so I write @people = Person.all.order(first_name: :asc).

# POST /people
# POST /people.json
def create
@person = Person.new(person_params)
@people = Person.all.order(first_name: :asc)

respond_to do |format|
  if @person.save
    format.js {flash.now[:notice] = "Person was successfuly created." }
  else
    format.js { flash.now[:notice] = "Could not create person."}
  end
end
end

The form submission is received by the create action, so I add create.js.coffee and in it I write the changes I want to make to the page. I add a presence validation to person.rb ensure the person has a first name.

<% if @person.errors.empty? %>
console.log "without errors"

$("#error_explanation").remove()
$(".people").html("<%= j render 'people/list', people: @people %>")

<% else %>
console.log "with errors"
$("#new_person").prepend("<%= j render 'layouts/flash', resource: @person %>")

<% end %>

The form builder gives the form an id of new_person and I use it to prepend the message. The Ah ha! moment I had while implementing the error display, is that errors are just like any other data and can be rendered in whatever way makes sense for the app. In the traditional request oriented approach, I would normally render the new view with @person which would cause the errors to be shown. In this case, the form is already on the page, so all I need to do is add the errors html. In this case, a little javascript does the trick. If I kept the error message html in my form partial, I could re-render the form with the errors. I chose to extract the error html out into partial, so I can reuse it as needed. If the submission is error free I remove the error html, in case it exists, and render the list.

Edit in Place

I want to be able to edit the data I enter into the app. Everything up to this point has been fairly straight forward. To create a new person, I fill out the form and press “Add”. How do make it so I click the person’s name on the list and the edit form opens? I didn’t want to add an edit link, so I dove into the source of jquery_ujs to see if I could reuse it’s code to make a remote call when I click the person’s name.

Rails’ jquery_ujs works behind the scenes to make it possible to submit the new form without changing pages. It watches <form>s and <a>s for the data attributes added by the view helpers when remote: true is passed as an option. These attributes provide the information needed to make the remote request.

I found my answer starting on line 131 of jquery_ujs in handleRemote. handleRemote is the method jquery_ujs class to make the ajax call when a remote link is clicked or form is submitted and is exposed by the plugin.

else {
  method = element.data('method');
  url = rails.href(element);
  data = element.data('params') || null;
}

The code above means that any element that’s not watched by the plugin with the attributes href, data-method, and optionally data-params can be used to make AJAX calls. All I need to do is add the attributes with the correct values to the element and call handleRemote when I want the request sent. With this knowledge, I add click handler on the person’s name.

# people.coffee
$ ->
  $(".people").on "click", ".person_fields", (e) ->
    console.log "Person has been clicked"
    $.rails.handleRemote($(this).parent())

Recall, _person.html.erb renders each list element with those attributes. With the click handler I make an AJAX call to the edit path of the person.

<!-- _person.html.erb -->
<%= content_tag_for :li, person, data: {remote: true, method: :get}, rel: "no-follow", href: edit_person_path(person), class: "list-group-item" do %>
...
<% end %>

In edit.js.coffee, I return a form (_edit_form.html.erb) styled to fit in the list element.

# edit.js.coffee
$("#<%= dom_id @person%>").replaceWith("<%= j render "people/edit_form", person: @person %>")

When the form is submitted, I respond with the javascript to update the list of people.

# people_controller.rb
# PATCH/PUT /people/1
# PATCH/PUT /people/1.json
def update
respond_to do |format|
  if @person.update(person_params)
    format.js { flash.now[:notice] = 'Person was successfully updated.' }
  else
  end
end
end
# edit.js.coffee
$("#<%= dom_id @person%>").replaceWith("<%= j render "people/edit_form", person: @person %>")

And there we have it! I created an app that used server generated javascript responses to prevent page changes and offer client side-esque interaction.

Conclusion

I now understand the value of DHH’s suggestion. It’s quite easy to make a dynamic app. My view code is reusable and I never have to implement a view in a javascript templating language. The remote requests follow the Rails patterns of new → create and edit → update. Now, I’m interested to see how ActionCable takes this approach to the next level.

For those wanting a closer look, the code is available on GitHub.

Tips and Tricks

  • Make use of the classes and ids generated by the helper methods and form builder
  • Use dom_id to make generating ids in your javascript/coffeescript easier
  • Use jQuery’s on method to attach event handlers to persistent elements in the DOM.

Tradeoffs

  • Your app is only as fast as the network

    In most cases the network is fast. When it isn’t, you can use progress indicators to indicate the app is still alive.

  • Loss of network connection makes it impossible for the app to work

    This is the same drawback as a “traditional” website. No internet means no pages are delivered, so it’s not really a problem unless you’re trying to create an offline app.


  1. Download the completed app.