December 26, 2010
#33 Using CoffeeScript
Here we'll take our JavaScript from Post #31 and convert it into CoffeeScript. (See Post #32 for some background.) In addition, we'll write another small CoffeeScript file to explore the syntax a little more.
Setting Up
We install The Ruby Racer and Barista gems. Since we'll make sure that all of our CoffeeScripts are compiled in development, we don't need these gems in production. (They probably won't work since we're using Heroku.) Then we get the CoffeeScript TextMate bundle for our system.
If we we wanted to change some of Barista's configuration (such as the location of the CoffeeScripts or where the compiled JavaScripts will be placed), we could run 'rails g barista:install' in our Terminal. This would create a Barista initializer file in config/initializers where we could make our changes. However, we'll stick to the defaults. (If we did make a Barista initializer file, we would have to exclude the code in that file from our production environment since the Barista gem was excluded for that environment.)
# Gemfile group :development, :test do gem 'therubyracer', :require => false gem 'barista', '0.7.0.pre3' ... end __________________________________________________ # Terminal > bundle > cd ~/Library/Application\ Support/TextMate/Bundles > git clone git://github.com/jashkenas/coffee-script-tmbundle CoffeeScript.tmbundle # optional > rails g barista:install
Converting post_show.js Into CoffeeScript
Let's see how our existing javascript file would look like in CoffeeScript. In the conversion, we've replaced 'function(){...}' with the '(...) ->' notation. We've also gotten rid of the semicolons, and instead rely on indentation for context. We also use '@' instead of 'this'. In addition, we don't use 'var' in variable declarations.
In makeContentClickable(), we use the '=>' notation. When we use '=>' instead of '->' we're changing the meaning of 'this' in the subsequent function definition. This allows us to use 'this' as if it were outside of the function. In our case, it will refer to PostAdmin instead of the jQuery objects. However, we avoid using '=>' when referencing jQuery's '$(this)' (as in _addHover() ).
PostAdmin = # type: 'section' or 'snippet' # selector: 'h2' or '.caption' makeContentClickable: ($editable, type, selector) -> editableId = $editable.attr('id').split('_')[1] $target = $editable.find(selector + ':first') $target.click => $.get("/" + type + "s/" + editableId + "/edit.js", (form) => @._insertForm($editable, type, form) @._addFormToggler($editable, $target) 'html') @._addHover($target, 'hover') _insertForm: ($editable, type, form) -> $editable .find('.' + type + 'Only:first') .after(form) _addFormToggler: ($editable, $target) -> $target .unbind('click') .click -> $editable.find('form:first').toggle() _addHover: ($target, klass) -> $target.hover -> $(this).toggleClass(klass) ajaxifyForms: (type, selector) -> $("form.simple_form." + type).live 'submit', -> form = $(this) $.post( form.attr('action') + ".js", form.serialize(), (response) -> PostAdmin._updatePost(form, response, selector) 'html') return false _updatePost: (form, response, selector) -> container = form.prev('div') # div.sectionOnly container .hide() .html(response) .fadeIn() $target = container.find(selector + ':first') @._addFormToggler(container.parent(), $target) @._addHover($target, 'hover') init: (opts) -> $.each opts, (type, selector) -> $('#content div.' + type).each -> PostAdmin.makeContentClickable($(this), type, selector) PostAdmin.ajaxifyForms(type, selector) $(window).load -> PostAdmin.init section: 'h2', snippet: '.caption'
Converting Our CoffeeScript Back To Javascript
Now we'll see how the file looks with after compilation. Barista will compile the CoffeeScript files if we run 'rake barista:brew' in Terminal or go to our app in a web browser. The first thing to notice is that the resulting code is wrapped in an anonymous function. "This safety wrapper, combined with the automatic generation of the var keyword, make it exceedingly difficult to pollute the global namespace by accident."
Next, we see a __bind() function has been generated. This is used when we use the '=>' notation so that we can bind 'this' inside of a function to 'this' outside of that function. Lastly, we see that all of our functions have explicit returns.
(function() {
var PostAdmin;
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
PostAdmin = {
makeContentClickable: function($editable, type, selector) {
var $target, editableId;
editableId = $editable.attr('id').split('_')[1];
$target = $editable.find(selector + ':first');
$target.click(__bind(function() {
return $.get("/" + type + "s/" + editableId + "/edit.js", __bind(function(form) {
this._insertForm($editable, type, form);
return this._addFormToggler($editable, $target);
}, this), 'html');
}, this));
return this._addHover($target, 'hover');
},
_insertForm: function($editable, type, form) {
return $editable.find('.' + type + 'Only:first').after(form);
},
_addFormToggler: function($editable, $target) {
return $target.unbind('click').click(function() {
return $editable.find('form:first').toggle();
});
},
_addHover: function($target, klass) {
return $target.hover(function() {
return $(this).toggleClass(klass);
});
},
ajaxifyForms: function(type, selector) {
return $("form.simple_form." + type).live('submit', function() {
var form;
form = $(this);
$.post(form.attr('action') + ".js", form.serialize(), function(response) {
return PostAdmin._updatePost(form, response, selector);
}, 'html');
return false;
});
},
_updatePost: function(form, response, selector) {
var $target, container;
container = form.prev('div');
container.hide().html(response).fadeIn();
$target = container.find(selector + ':first');
this._addFormToggler(container.parent(), $target);
return this._addHover($target, 'hover');
},
init: function(opts) {
return $.each(opts, function(type, selector) {
$('#content div.' + type).each(function() {
return PostAdmin.makeContentClickable($(this), type, selector);
});
return PostAdmin.ajaxifyForms(type, selector);
});
}
};
$(window).load(function() {
return PostAdmin.init({
section: 'h2',
snippet: '.caption'
});
});
}).call(this);Another Example - Countdown Timer
To play with the CoffeeScript syntax a little more, we write some code for a countdown timer. (The code is based off of a Teach Me To Code (TMTC) screencast ). We can create classes using 'class' in similar fashion to Ruby. Within the class, the constructor() function behaves similarly to Ruby's #initialize, and we can set some default values for our arguments while simultaneously define some 'instance' variables.
Looking at init(), in the TMTC screencast 'timer.tick()' was passed to setInterval(). This required that 'timer' be a global variable. Since CoffeeScript tries to make it hard to use global variables, we have to explicitly attach it to the window object in order to use it and avoid variable scope problems.
In tick(), we are able to perform parallel variable assignments. Also we can use 'or' and 'is' in logic expression. In updateTarget(), we use 'if' at the end of an expression, again similarly to Ruby.
If we compare this CoffeeScript code to the resulting JavaScript , we can see that CoffeeScript is much more readable.
class Countdown constructor: (@target_id = "#timer", @start_time = "30:00") -> init: -> @reset() window.tick = => @tick() setInterval(window.tick, 1000) reset: -> time = @start_time.split(':') @minutes = parseInt(time[0]) @seconds = parseInt(time[1]) @updateTarget() tick: -> [seconds, minutes] = [@seconds, @minutes] if seconds > 0 or minutes > 0 if seconds is 0 @minutes = minutes - 1 @seconds = 59 else @seconds = seconds - 1 @updateTarget() updateTarget: -> seconds = @seconds seconds = '0' + seconds if seconds < 10 $(@target_id).html(@minutes + ":" + seconds)
Adding A Demo Countdown Timer
We'll display the countdown timer in this post so we can make sure it works. First we add some code to insert a demo div before the comments section. Then we initialize a new Countdown object when the div is clicked.
We add the JavaScript to our post show page. (The logic regarding which scripts to include into this page is moved into a helper method.)
# countdown.coffee class Countdown ... $ -> $('ol.comments:first').before('<div id="demo">Click me for demo</div>') $('#demo').click -> timer = new Countdown('#demo') timer.init() $('#demo').unbind('click') __________________________________________________ # posts_helper.rb module PostsHelper def show_javascripts files = ['post_show'] files << 'admin/post_show' if admin? files << 'countdown' if @post.sequence == 33 javascript files end ... __________________________________________________ # show.html.haml - show_javascripts ...
(Please login to submit comments.)
Comments