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 inpublic/javascripts/companies.js
. - I have the path
/companies/liveseach?query=
hardcoded inpublic/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 inpublic/javascripts
. But in my application I also have some.js.erb
file in myapp/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_f开发者_高级运维or(@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 thevote
andunvote
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!
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.
精彩评论