If you can use turbolinks (e.g. brand new project), use it and read no further. It’ll cost you nothing and will get you a long way before you need anything more sophisticated.
Like so many others, the website I am currently working on is using client side js and ajax to make user experience snappier by not reloading page when they click links and buttons. But, ultimately, every user action results in a server call. No state is client side only.
To do this, we intercept form submissions and link clicks in js, turn them into $.ajax
and handle the json or html returned from the server in success handler. Pretty standard. And pretty repetitive too. Why so? Because every time code follows the same pattern of intercepting the user event, rebinding to $.ajax
and handing the response. Over and over again.
Rails is good in shortcutting common tasks away. And this case is no exception. Below I’ll demonstrate what rails has to offer using a simplified example of posting a comment into the comments thread (source code)
Let us start with a non-ajax version (commit):
controllers/comments_controller.rb:
def index
@comments = Comment.all
end
def create
Comment.create! comment_params
redirect_to action: :index
end
views/comments/index.html.erb:
<%= form_for Comment.new do |f| %>
<%= f.text_area :text %>
<%= f.submit %>
<% end %>
<%= render @comments %>
views/comments/_comment.html.erb:
<div class="comment">
<span><%= comment.text %></span>
<span><%= comment.created_at %></span>
</div>
Now let us ajaxify adding new comment.
Intercept form submission and turn it into an ajax call that returns json (commit):
comments/comments_controller.rb:
def create
comment = Comment.create! comment_params
respond_to do |format|
format.html { redirect_to action: :index }
format.json { render json: comment }
end
end
assets/javascripts/comments.js.coffee:
$(document).on "submit", "#new_comment", (e) ->
e.preventDefault()
$form = $ this
$.post "#{$form.attr 'action'}.json", $form.serializeArray(), (comment) ->
$text = $('<span>').text comment.text
$createdAt = $('<span>').text comment.created_at
$newComment = $('<div class="comment">').append($text).append $createdAt
$('.comment:last').after $newComment
This might not be the best way of client side rendering, but it immediately highlights the problem of duplicate rendering of the same thing - comment - on the client and on the server. So, instead of returning json, let us reuse server side template and return html instead.
controllers/comments_controller.rb:
def create
comment = Comment.create! comment_params
if request.xhr?
render comment
else
redirect_to action: :index
end
end
assets/javascripts/comments.js.coffee:
$(document).on "submit", "#new_comment", (e) ->
e.preventDefault()
$form = $ this
$.post $form.attr('action'), $form.serializeArray(), (html) ->
$('.comment:last').after html
That is where we normally stop. Our client side javascript is largely a combination of the two approaches above: json and html. But there is a third one where rails returns javascript that automatically gets evaled (by jquery-ujs) on successful response. Sounds evil. But let us see it in action.
controllers/comments_controller.rb:
def create
@comment = Comment.create! comment_params
respond_to do |format|
format.html { redirect_to action: :index }
format.js
end
end
views/comments/create.js.coffee
$('.comment:last').after '<%= j render @comment %>'
We also need to add remote: true
to form_for
in comments index.
So why is this better than returning html?
views/comments
) and named after the action - create.js.coffee
.Sometimes you need to run javascript before the ajax request is sent. Typical example would be to disable user control (link or button) while request is going on and maybe show a loading spinner or something. Rails provides a simple shortcut for disabling controls: remote buttons and links accept disable_with
option that will disable a control while request is in progress optionally changing its text.
Also, jquery-ujs broadcasts a number of events which can be used to trigger global loading indicator or something along those lines.
Obviously they can also be bound to specific links, forms and buttons but that is where it stops being pretty :)
UPDATE (17 July 2014)
Josh Chisholm suggested in comments a variation of js approach in which AccountsController#create
redirects to index
instead of rendering javascript (commit). The nice thing about it, is that there is no need for different respond_to
based on type of request (html or js) in create
. Request processing follows the same path regardless.