I've recently read that embedding ruby inside JavaScript is not a good idea. However, in books such as David Heinemeier Hansson's Agile Web Development with Rails, that's exactly what it does. If embedding ruby with JS is NOT a good a idea, then what's the best practice for such case? Given something as simple as this: (jQuery + ruby)
posts_controller
def create
@post = Post.new(params[:post])
respond_to do |format|
if @post.save
format.html { redirect_to(@post, :notice => 'Post was successfully created.') }
format.js #will use this response to process ajax
else
format.html { render :action => "new" }
end
end
end
create.js.erb
$tr = $('<tr>');
$td1 = $('<td>').text('<%= @post.title %>');
$td2 = $('<td>').text('<%= @post.content %>');
$tr.append($td1, $td2);
$('table tbody').append($tr);
How should it be refactored to follow the "best practice" of not embedding ruby with JS?(if that's the case)
I really need to be enlightened on this, and maybe get the concepts right because I have read that rails 3.1 will separate JS from Ruby completely through assets ?(Is this correct?)
Thanks !
RJS templates were essentially the way things were done for a long time, which is why you still see such a prevalence of the technique in tutorials. That being said, as you've noticed, they're on the way out, and with good reason.
I'm sure there are many reasons why RJS templates are a very bad thing, but a big one is how tightly they couple your view JS to your view HTML. Many problems arise from this, a few being:
Lack of flexibility.
Using your bit of code as an example. What if you wanted to be able to create posts from a different view? And have a different effect? Maybe there's no <table>
on the page at all and you just want to pop up a "success" message? Etc.
What if you just wanted a data representation of your posts, but your javascript templates were written to manipulate HTML?
How do you handle all this in one create.js.erb
, which is tightly coupled to, most likely, the posts new.html.erb
template?
Complexity.
RJS templates operate in somewhat of a vacuum. Generated on server side, their execution is not bound to DOM events. This makes it tricky to do things like, say, update a <form>
on a page after an object is being created, as you have no frame of reference to select the appropriate <form>
in the JS template (e.g. <form id="new_post_123">
). There are workarounds for this, but they're more convoluted than they need to be.
By binding to a form client-side and dealing with the result, this problem is eliminated. You don't need to find the proper form to update after a response.
With RJS you have no such binding, and you're forced to use the known structure of the HTML to retrieve and manipulate the appropraite elements. One example of unnecessary complexity.
I really need to be enlightened on this, and maybe get the concepts right because I have read that rails 3.1 will separate JS from Ruby completely through assets ?(Is this correct?)
This is essentially true. The asset pipeline, while cool, doesn't offer anything brand new. Asset frameworks like Sprockets and Sass, and the workflows they enable, have been around for a while. The Rails 3.1 asset pipeline just brings them into standard practice. It's more a matter of perspective, as DHH talked about in his recent RailsConf keynote. By bringing greater organization to these assets, js files in particular, it makes them feel more like first-class citizens, so to speak, deserving of as much attention as your back-end code. As opposed to the "junk drawer" feel of public/javascripts
.
You might do it something like this (although untested and a bit simplified, e.g. in Rails 3 you might use responders for this rather than a respond_to block:
# as you have, but returning the object as data which can be handled client-side,
# rather than RJS generated by the server
def create
@post = Post.new(params[:post])
respond_to do |format|
if @post.save
format.js { render :json => @post }
else
# ...
end
end
end
// Then in your client side JS, a handler for your remote form.
$(function(){
$('#your-create-form').bind('ajax:success', function(data, status, xhr) {
// simply repeating what you had in the template, replacing the erb with
// attributes from the returned json
$tr = $('<tr>');
$td1 = $('<td>').text(data.title);
$td2 = $('<td>').text(data.content);
$tr.append($td1, $td2);
$('table tbody').append($tr);
});
}
If you embed dynamic data in JavaScript files, then you often have to give it caching rules suitable for content that can change frequently.
If you need to process dynamic content with JS then it is usually better to have a static, cacheable, reusable script that gets data from elsewhere.
In this specific example, although I'm not sure since there isn't a great deal of context, it looks like you would be better off generating HTML directly instead of generating JS that generates HTML.
I'm sure you can find this question asked by beginning PHP devs, Perl, ASP and whatever other server-side languages there are out there. There don't seem to be too many cases where you'd want to have your javascript code inline.. the only one might be where you're customizing it to a particular situation, but off the top of my head I can't think of any situations where you'd need to customize it like that.
Peformance-wise, if you have a lot of javascript and you include it all in your output, you bloat your page. A 1k page will turn into a >100k page if you're adding a big application in. And, along with that, it will likely be more difficult to cache that information since you're probably customizing the output just a little bit if the user is logged in, or if there's a special ad out that day, etc. You're much better off splitting it off into its own file so that you can "minimize" it (http://en.wikipedia.org/wiki/Minification_%28programming%29) as well.
From a developer to a developer, I can give you horror stories of having javascript (and html) embedded in both PHP and Smarty templates. Be nice to whoever you're working with and split all the languages out into their own files. Let's say you're working with Bob and he's a great javascript guy.. but doesn't know a thing about the application and how it spits out its code. Having javascript smattered in the erb's is going to be frustrating due to the lack of a single point of truth for the application - if he wants to tweak something, he's going to be looking through all your erb's to figure out where exactly he has to put it or where was it originally. And do you really want someone who knows little about your backend's structure to be poking around through those files?
There's a couple of cases though where you'd want to put bootstrap data in.. if you've got a lot of data to inject to the page on startup, having that assigned to a few variables when the page starts is more performant than going out and making an ajax request on every page load.
Please be nice to us front-end guys and unless you've got a real good reason.. keep the JS in separate files :)
Edit: Here's an example of doing what you want (I think), without really using templated javascript:
app.js:
postUpdater = function (data) {
$tr = $('<tr>');
$td1 = $('<td>').text(data.title);
$td2 = $('<td>').text(data.content);
$tr.append($td1, $td2);
$('table tbody').append($tr);
};
$("#save_button").click(function () {
$.post('/save/ur/', {the:data}, postUpdater);
});
In ruby, you'll want to just render the @post to json.. I think it's literally @post.to_json, but you may have to require 'json'
in order to get it to work. This might get you there faster though: output formated json with rails 3 (I'm not really a ruby guy.. we just use it at the company I work for so of course I've been picking it up)
Now, as for a theoretical on why not to just output a templated javascript file. Let's say you've got a whole bunch of stuff that needs to happen when you save that javascript file. That means you've got to output a whole lot of stuff on that ajax request and execute it. You'd be pushing a larger file over the internet (a tiny bit slower) and possibly having to 'eval' a response (eval is evil). So, that's one bad thing. Another possibly bad thing is that you lose access to where it was you called the save from. So, if I've got this view..
new PostViewer({
savePost: function () {
var self = this;
$.post('/save/url/', {the:data}, function (response) {
self.trigger('saveSuccessful', response);
});
}
});
When the savePost gets called it will be more difficult (without possibly introducing numerous hacks) to simply tell the view "Hey, the save was successful!".
Hopefully that's a bit more on target for what you were looking for.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With