Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Improve unobtrusive javascript (and possibly use CoffeeScript) in a Rails application

I have an application which uses some Javascript for basic Ajax requests such as autocompletion and live search. For example I implemented live search in the following way; I spotted some potential issue and would like to talk with you about it, so to have a better code.

app/controllers/company_controller.rb

def livesearch
  @companies = Company.search(params[:query])
  render :partial => "companies", :locals => {:companies => @companies}
end

app/views/companies/_companies.html.haml

- if companies.empty?
  None
- else
  %table#company_list
    %tr
      %th Name
      %th Description
      %th Products
      %th
    = render companies

app/views/companies/_livesearch_box.html.haml

= content_for :scripts, "jlivesearch companies"
= form_tag "#", :autocomplete => :off, :remote => true do
  %span.light
    Search:  
  = text_field_tag :search
  :javascript
    $('#search').livesearch({
      searchCallback: update_listed_companies,
      queryDelay: 200,
      innerText: "Search companies"
    });

public/javascripts/companies.js

function update_listed_companies(query) {
  if(typeof query == "undefined")
    query = "";

  $("#company_list_container").showWith(
    "/companies/livesearch?query=" + query,
    false
  );
}

public/javascripts/application.js

(function($) {
  $.fn.showWith = function (what, popup) {
    element = this;
    $.get(what, function(data) {
      element.html(data);
      if(popup)
        element.bPopup();
    });
    return element;
  };
})(jQuery);

Here are the things that make me suspicious about the optimality of my code:

  • I have Javascript code in _livesearch_box.html.haml.
  • Even if I put it in a public/javascripts/companies_livesearch.js I would have to hardcode the #search part in it.
  • I have #company_list_container (which is the div in which _companies.html.haml is rendered) hardcoded in public/javascripts/companies.js.
  • I have the path /companies/liveseach?query= hardcoded in public/javascript/companies.js.
  • I'm not using CoffeeScript, mainly because it expects (at least if you use Barista) to find pure javascript code somewhere (e.g. in app/coffeescripts/) and compiles it in public/javascripts. But in my application I also have some .js.erb file in my app/views/companies; for example, I have a voting system that uses the following in app/views/companies/_vote.js.erb: $("#vote_link_<%= escape_javascript(@company.id.to_s) %>").html("<%= escape_javascript(vote_link_for(@company)) %>") To replace the "Vote this company" link with the "Unvote this company" one (and vice-versa) with an Ajax request and is rendered by the vote and unvote actions in the controller. I know that there is coffee-haml-filter that compiles CoffeeScript inside haml files but it's not what I exactly need and is usually deprecated and regarded as something dirty (?).

So the questions are at least:

  • How to have CoffeeScript in my app/views/*/*.js.*?
  • Should I have app/views/*/*.js.* files at all?
  • How to remove all those element ids and those paths hardcoded in javascripts in the most efficient and elegant way?

Sorry for the long question and thanks for getting to the end of it!

like image 394
Alberto Santini Avatar asked Mar 14 '11 14:03

Alberto Santini


1 Answers

Routes

There are some solutions like js-routes (my fork) which will allow you to write Router.post_path(3) in your JS/CS. This way you can get around hardcoding urls.

Mixing JS and ERB

I would advise you to avoid mixing JS and Ruby. In most cases you can get around that by refactoring your JS code, the result will be easier to read and can simply be moved into a pure JS/CS-file.

# based on your vote-link example and assuming that your link
# looks like:
#
# %a(href="#"){:"data-company-id" => @company.id} Vote
# => <a href="#" data-company-id="6">Vote</a>

makeAllCompaniesVotable: () ->
  $('.company a.voteLink').click ->
    companyId = $(this).data('company-id')
    $.ajax
      url: Router.vote_company_path(companyId)
      # ...

Unless you do evil eval-magic, you won't even need escape_javascript. But you will have to remove the JavaScript from inside your partials. jquery.livequery made the transition easier.

$(`.company`).livequery ->
  # do something with $(this)

will be called each time a .company is inserted into the document.

Hardcoding DOM-Paths

If you are writing code for a specific dom-tree (or a specifc view) I wouldn't consider it a bad practice. Writing unobtrusive JS is like writing CSS - and we hardcode #company_list_container in CSS too, don't we?

$("#vote_link_<%= escape_javascript(@company.id.to_s) %>") # this is ugly though

Calling the JS code from the frontend

To have an interface between the static CoffeeScript-files and the views I tend to write something like:

:javascript
  $(function(){Companies.index()});
  $(function(){Application.globalEnhancements()});

at the end of my views. This will then call a function I wrote with CoffeeScript, which will then enhance the site with all the needed scripts. There might be better approaches (like having a Rails-like Router for JavaScript - see Backbone.js) but it's simple and works for me.

Also if I need some data quite often (for example: the current_user):

:javascript
  window.current_user = #{current_user.to_json};

However I don't think there is an efficient way to refactor. I had to do a lot of refactoring to get my ERB/JS mess removed. Still worth, though.

like image 91
Marcel Jackwerth Avatar answered Nov 06 '22 13:11

Marcel Jackwerth