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, Terminal
# 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() ).

app/coffeescripts/admin/post_show.coffee
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.

public/javascripts/admin/post_show.js
(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.

app/coffeescripts/countdown.coffee
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.)

app/coffeescripts/countdown.coffee, app/helpers/posts_helper.rb, app/views/posts/show.html.haml
# 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
...

    Comments

(Please login to submit comments.)



View All Posts