A. Neumann A. Neumann - 1 month ago 13
Ajax Question

How to respond_to AJAX call without erb.js / why use erb.js?

The last few days I've spent trying to understand different facets of AJAX on rails. After reading some introductions I've managed to get an idea of the rails built-in UJS feature. An example from a small toy app I wrote and where I want to introduce some AJAX-capability...

The controller action looks as follows

class ExpenseListsController < ApplicationController
before_action :require_authentication

...

def create
@expense_list = ExpenseList.new(expense_list_params)
if @expense_list.save
respond_to do |format|
format.html do
flash[:success] = 'Created!'
render :show
end
format.js
end
else
respond_to do |format|
format.html do
@errors = @expense_list.errors
flash[:danger] = 'Something went wrong!'
render :new
end
format.js
end
end
end

...

end


In my view I call the action via the
remote: true
option

The respective
create.js.erb
looks like this

var form_field = $('.expense_lists_form');
var expenseLists = $('#expense-lists');

expenseLists.append("<%= j render @expense_list %>");
form_field.slideUp(200);


The template for
@expense_list
looks like this

.col-xs-12.col-lg-3.col-md-4{id: "expense_list_#{expense_list.id}"}
.panel.panel-default
.panel-heading
= link_to expense_list.name, expense_list_path(expense_list)
.panel-body
.links
= link_to 'Modify list', edit_expense_list_path(expense_list), remote: true
= link_to 'Delete list', expense_list_path(expense_list), method: 'delete', remote: true
.description
%p
- if expense_list.description.present?
= expense_list.description
- else
%i
No description supplied, add one
=link_to 'here', edit_expense_list_path(expense_list)
.email-notification.text-muted
(Email notifications enabled)
.panel-footer
= "Expenses in #{current_month_name}:"
%b
= "#{expense_list.sum_of_exp_in_month(current_month, current_year)}€"
= "(#{expense_list.euros_left_in_month(current_month, current_year)}€ left)" if expense_list.budget_in_euro


It works but to me this idea seems to have some down-sides:


  • Bloats my file structure by having extra *.js.erb files

  • Distorts the physical separation of JS and the rest of the codebase

  • As I use HAML it introduces a new style of coding (ERB)



Now I have two questions:


  1. Every tutorial (I've seen so far) seems to promote this kind of solution for handling AJAX responses in Rails: Why? When I check the code of other, larger rails projects (e.g. Diaspora) I do not seem to find them do it this way - most of them seem to handle it inside plain JS/jQuery via
    $.ajax({ ... })
    . So what would be the major advantages of the rails-internal UJS approach?

  2. If the rails UJS-way is preferrable for some reason: How do you organise your code? Create extra directories for the
    *.js.erb
    -files?

  3. What would be a good practice to transfer all this stuff to plain javascript files located in my
    /app/assets/javascript
    directory and handle the AJAX requests within jQuery there? How would my controller response have to look like in order to respond with the proper portion of HTML to update the DOM with via JS? In other words: How can I respond with a partial that I can handle in Plain Javascript/jQuery?



Thanks in advance!
Andi

max max
Answer

So what would be the major advantages of the rails-internal UJS approach?

js.erb is a form of a poor mans Single Page Architecture (SPA).

It makes it just easy enough to return .js responses from your controller which modify the current page and lets you use the rails helpers for templating so that you don't have to use a client side templating system such as handlebars.

Note that this is not really internal to rails. jQuery UJS simple uses the fact that rails can return multiple formats of a resource. You could potentially use this with any MVC frameworks that can serve javascript.

The main advantage is that it is very approachable. Its just enough for classic syncronous apps that want a few sprinkles of ajax here and there.

It gives developers who think jQuery.load and script tags everywhere is the best thing since sliced bread just enough rope to hang themselves with.

The cons

  • Violates REST as js.erb views are usually used as procedures to manipulate the current page.
  • the javascript in a js.erb view is not minified by the assets pipeline as it served per request.
  • It leads to horrible architecture decisions.

Whats the alternative?

Long before jquery-ujs entered the scene we had already figured out that the best way to do ajax requests is JSON.

So if you want to send a form asynchronously you would do:

$(document).on('submit', '.ajax-form', function(e){
  e.preventDefault();
  var $form = $(this);
  var promise = $.ajax($form.attr('action'), {
    accepts: { json: 'application/json' },
    data: $form.serialize(),
    context: $form,
    method: $form.attr('method')
  });
  promise.done(function(response){
    // handle the response
  });
});

This way the javascript logic can be concatenated into a single file, and tested separately in javascript testing tools.

Your backend server just responds with simple data and does not concern itself with what the client does with it.

However this does require you to setup some sort of templating on the client side to handle converting JSON to HTML and you need to setup stuff like data bindings to have your form display errors. This leads to code duplication.

This is where SPA frameworks like Ember and Angular come into the picture which do all the templating and rendering in the client.

How can I respond with a partial that I can handle in Plain Javascript/jQuery?

You could create add additional formats which your controller responds to. You could for example register a "text/html-partial" mime type.

Or create additional routes or even use a query param (shudder).

However this is less than ideal for the exact same reason as js.erb - it leads to a crappy API as your controllers will become process instead of resource oriented. You will end up creating ridiculous controller actions just to pass html fragments back to the client.