Raimonds SimanovskisRails-like JavaScriptusing CoffeeScript,Backbone.js andJasmine
Raimonds Simanovskis       github.com/rsim         @rsim             .com
The Problem
Ruby code in Rails
JavaScript code in   Rails 3.0.x
application.js// Place your application-specific JavaScript functions and classes here// This file is automatically includ...
Which leads to...(example from Redmine)
application.js                  (example from Redmine)/* redMine - project management software   Copyright (C) 2006-2008 J...
/* * 1 - registers a callback which copies the csrf token into the * X-CSRF-Token header with each ajax request. Necessary...
The Problem #2
Do we really know    (and love?)   JavaScript?
Sample JavaScript               (from RailsCasts #267)var CreditCard = {  cleanNumber: function(number) {    return number...
We see as this                  “ugly” RubyCreditCard = {  :cleanNumber => lambda { |number|    return number.gsub(/[- ]/,...
Or as this “normal” Rubymodule CreditCard  def self.clean_number(number)    number.gsub(/[- ]/, "")  end  def self.valid_n...
“Best practices” Rubyclass CreditCard  def initialize(number)    @number = clean_number(number)  end  def valid?    total ...
JavaScript has objects too!var CreditCard = function(number) {   function cleanNumber(number) {     return number.replace(...
But this would be much        more Ruby-like!class CreditCard  cleanNumber = (number) -> number.replace /[- ]/g, "" constr...
Sample CoffeeScript# Assignment:                   # Splats:number   = 42                   race = (winner, runners...) ->...
Functionssquare = (x) -> x * xcube   = (x) -> square(x) * xfill = (container, liquid = "coffee") ->  "Filling the #{contai...
Objects and Arrays song = ["do", "re", "mi", "fa", "so"] singers = {Jagger: "Rock", Elvis: "Roll"} bitlist   = [   1, 0,  ...
Variable Scope                          var changeNumbers, inner, outer;outer = 1                 outer = 1;changeNumbers ...
Existential Operator solipsism = true if mind? and not world? speed ?= 75 footprints = yeti ? "bear" zip = lottery.drawWin...
Conditionalsmood = greatlyImproved if singingif happy and knowsIt  clapsHands()  chaChaCha()else  showIt()date = if friday...
Loopseat food for food in [toast, cheese, wine]countdown = (num for num in [10..1])earsOld = max: 10, ida: 9, tim: 11ages ...
Classes, Inheritance     and super  class Animal    constructor: (@name) ->    move: (meters) ->      alert @name + " move...
Function BindingAccount = (customer, cart) ->  @customer = customer  @cart = cart  $(.shopping_cart).bind click, (event) =...
And many othernice features...
How to install?brew install node # or install node.js otherwisecurl http://npmjs.org/install.sh | shnpm install -g coffee-...
Back to theProblem #1
Dynamic single page    application
Identifyingcomponents    AppView
Identifyingcomponents    AppView              TodoView              TodoView              TodoView
Identifyingcomponents          AppViewkeypress eventclick event          TodoView      dblclick event TodoView            ...
Browser-side       Views and Models          AppView                                   TodoListkeypress eventclick event  ...
Browser-side       Views and Models          AppView                                   new, fetch                         ...
Browser-side      Views and Models         AppView                               refresh, add    TodoListkeypress eventcli...
Browser-side Modelsand RESTful resourcesBrowser                  Rails             GET TodoList            TodosController...
Organize CoffeeScriptand JavaScript Code using http://github.com/Sutto/barista
application.coffee    # main namespace    window.TodoApp = {}
Todo modelclass TodoApp.Todo extends Backbone.Model # If you dont provide a todo, one will be provided for you. EMPTY: "em...
TodoList collectionclass TodoApp.TodoList extends Backbone.Collection # Reference to this collections model. model: TodoAp...
Todo item viewclass TodoApp.TodoView extends Backbone.View  # ... is a list tag.  tagName: "li" # Cache the template funct...
# Re-render the contents of the todo item.render: ->  $(@el).html @template @model.toJSON()  @setContent()            Todo...
Application viewclass TodoApp.AppView extends Backbone.View # Instead of generating a new element, bind to the existing sk...
@collection.bind add,     @addOne @collection.bind refresh, @addAll @collection.bind all,     @renderStats       Applicati...
view = new TodoApp.TodoView model: todo @$("#todo-list").append view.render().el# Add all items in the collection at once....
#todoapp                  index.html.haml  .title    %h1 Todos  .content    #create-todo      %input#new-todo{:placeholder...
%input#new-todo{:placeholder => "What needs to be done?", :type => "text"}/      %span.ui-tooltip-top{:style => "display:n...
One more thing:Backbone Controllers          Routersclass Workspace extends Backbone.Controller Router  routes:    "help" ...
How do you test it?
RSpec-like testing for    JavaScript
Together with all  other tests
Testing Todo modeldescribe "Todo", ->  todo = null  ajaxCall = (param) -> jQuery.ajax.mostRecentCall.args[0][param]  befor...
and TodoList                  collectiondescribe "TodoList", ->  attributes = [    content: "First"    done: true  ,    co...
Rails 3.1Asset Pipeline
application.js.coffee    using Sprockets     #=   require jquery     #=   require underscore     #=   require backbone    ...
Watch RailsConf           DHH keynotehttp://en.oreilly.com/rails2011/public/schedule/detail/19068
References     http://jashkenas.github.com/coffee-script/    http://documentcloud.github.com/backbone/         http://pivo...
Used in eazybi.com
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Upcoming SlideShare
Loading in...5
×

Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine

27,007

Published on

Presented at RailsWayCon 2011

2 Comments
42 Likes
Statistics
Notes
No Downloads
Views
Total Views
27,007
On Slideshare
0
From Embeds
0
Number of Embeds
9
Actions
Shares
0
Downloads
267
Comments
2
Likes
42
Embeds 0
No embeds

No notes for slide

Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine

  1. 1. Raimonds SimanovskisRails-like JavaScriptusing CoffeeScript,Backbone.js andJasmine
  2. 2. Raimonds Simanovskis github.com/rsim @rsim .com
  3. 3. The Problem
  4. 4. Ruby code in Rails
  5. 5. JavaScript code in Rails 3.0.x
  6. 6. application.js// Place your application-specific JavaScript functions and classes here// This file is automatically included by javascript_include_tag :defaults
  7. 7. Which leads to...(example from Redmine)
  8. 8. application.js (example from Redmine)/* redMine - project management software Copyright (C) 2006-2008 Jean-Philippe Lang */function checkAll (id, checked) { var els = Element.descendants(id); for (var i = 0; i < els.length; i++) { if (els[i].disabled==false) { els[i].checked = checked; } }}function toggleCheckboxesBySelector(selector) { boxes = $$(selector); var all_checked = true; for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } } for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; }}function setCheckboxesBySelector(checked, selector) { var boxes = $$(selector); boxes.each(function(ele) { ele.checked = checked; });}function showAndScrollTo(id, focus) { Element.show(id); if (focus!=null) { Form.Element.focus(focus); }
  9. 9. /* * 1 - registers a callback which copies the csrf token into the * X-CSRF-Token header with each ajax request. Necessary to application.js * work with rails applications which have fixed * CVE-2011-0447 * 2 - shows and hides ajax indicator */Ajax.Responders.register({ (example from Redmine) onCreate: function(request){ var csrf_meta_tag = $$(meta[name=csrf-token])[0]; if (csrf_meta_tag) { var header = X-CSRF-Token, token = csrf_meta_tag.readAttribute(content); if (!request.options.requestHeaders) { request.options.requestHeaders = {}; } request.options.requestHeaders[header] = token; } if ($(ajax-indicator) && Ajax.activeRequestCount > 0) { Element.show(ajax-indicator); } }, onComplete: function(){ if ($(ajax-indicator) && Ajax.activeRequestCount == 0) { Element.hide(ajax-indicator); } }});function hideOnLoad() { $$(.hol).each(function(el) { el.hide(); });}Event.observe(window, load, hideOnLoad);
  10. 10. The Problem #2
  11. 11. Do we really know (and love?) JavaScript?
  12. 12. Sample JavaScript (from RailsCasts #267)var CreditCard = { cleanNumber: function(number) { return number.replace(/[- ]/g, ""); }, validNumber: function(number) { var total = 0; number = this.cleanNumber(number); for (var i=number.length-1; i >= 0; i--) { var n = parseInt(number[i]); if ((i+number.length) % 2 == 0) { n = n*2 > 9 ? n*2 - 9 : n*2; } total += n; }; return total % 10 == 0; }};console.log(CreditCard.validNumber(4111 1111-11111111)); // trueconsole.log(CreditCard.validNumber(4111111111111121)); // false
  13. 13. We see as this “ugly” RubyCreditCard = { :cleanNumber => lambda { |number| return number.gsub(/[- ]/, ""); }, :validNumber => lambda { |number| total = 0; number = CreditCard[:cleanNumber].call(number); for i in 0..(number.length-1) n = number[i].to_i; if ((i+number.length) % 2 == 0) n = n*2 > 9 ? n*2 - 9 : n*2; end total += n; end; return total % 10 == 0; }};puts(CreditCard[:validNumber].call(4111 1111-11111111)); # trueputs(CreditCard[:validNumber].call(4111111111111121)); # false
  14. 14. Or as this “normal” Rubymodule CreditCard def self.clean_number(number) number.gsub(/[- ]/, "") end def self.valid_number?(number) total = 0 number = clean_number(number) for i in 0...number.length n = number[i].to_i if i+number.length % 2 == 0 n = n*2 > 9 ? n*2 - 9 : n*2 end total += n end total % 10 == 0 endendputs CreditCard.valid_number?(4111 1111-11111111) # trueputs CreditCard.valid_number?(4111111111111121) # false
  15. 15. “Best practices” Rubyclass CreditCard def initialize(number) @number = clean_number(number) end def valid? total = 0 for i in 0...@number.length n = @number[i].to_i if i+@number.length % 2 == 0 n = n*2 > 9 ? n*2 - 9 : n*2 end total += n end total % 10 == 0 end private def clean_number(number) number.gsub(/[- ]/, "") endendputs CreditCard.new(4111 1111-11111111).valid? # trueputs CreditCard.new(4111111111111121).valid? # false
  16. 16. JavaScript has objects too!var CreditCard = function(number) { function cleanNumber(number) { return number.replace(/[- ]/g, ""); } this.number = cleanNumber(number);};CreditCard.prototype = { isValid: function() { var total = 0; for (var i=this.number.length-1; i >= 0; i--) { var n = parseInt(this.number[i]); if ((i+this.number.length) % 2 == 0) { n = n*2 > 9 ? n*2 - 9 : n*2; } total += n; }; return total % 10 == 0; }};console.log( (new CreditCard(4111 1111-11111111)).isValid() ); // trueconsole.log( (new CreditCard(4111111111111121)).isValid() ); // false
  17. 17. But this would be much more Ruby-like!class CreditCard cleanNumber = (number) -> number.replace /[- ]/g, "" constructor: (number) -> @number = cleanNumber number isValid: (number) -> total = 0 for i in [0...@number.length] n = +@number[i] if (i+@number.length) % 2 == 0 n = if n*2 > 9 then n*2 - 9 else n*2 total += n total % 10 == 0console.log (new CreditCard 4111 1111-11111111).isValid() # trueconsole.log (new CreditCard 4111111111111121).isValid() # false
  18. 18. Sample CoffeeScript# Assignment: # Splats:number = 42 race = (winner, runners...) ->opposite = true print winner, runners# Conditions: # Existence:number = -42 if opposite alert "I knew it!" if elvis?# Functions: # Array comprehensions:square = (x) -> x * x cubes = (math.cube num for num in list)# Arrays:list = [1, 2, 3, 4, 5]# Objects:math = root: Math.sqrt square: square cube: (x) -> x * square x
  19. 19. Functionssquare = (x) -> x * xcube = (x) -> square(x) * xfill = (container, liquid = "coffee") -> "Filling the #{container} with #{liquid}..."awardMedals = (first, second, others...) -> gold = first silver = second rest = otherscontenders = [ "Michael Phelps" "Liu Xiang" "Yao Ming" "Allyson Felix" "Shawn Johnson"]awardMedals contenders...
  20. 20. Objects and Arrays song = ["do", "re", "mi", "fa", "so"] singers = {Jagger: "Rock", Elvis: "Roll"} bitlist = [ 1, 0, 1 0, 0, 1 1, 1, 0 ] kids = brother: name: "Max" age: 11 sister: name: "Ida" age: 9
  21. 21. Variable Scope var changeNumbers, inner, outer;outer = 1 outer = 1;changeNumbers = -> changeNumbers = function() { var inner; inner = -1 inner = -1; outer = 10 return outer = 10;inner = changeNumbers() }; inner = changeNumbers();
  22. 22. Existential Operator solipsism = true if mind? and not world? speed ?= 75 footprints = yeti ? "bear" zip = lottery.drawWinner?().address?.zipcode
  23. 23. Conditionalsmood = greatlyImproved if singingif happy and knowsIt clapsHands() chaChaCha()else showIt()date = if friday then sue else jilloptions or= defaults
  24. 24. Loopseat food for food in [toast, cheese, wine]countdown = (num for num in [10..1])earsOld = max: 10, ida: 9, tim: 11ages = for child, age of yearsOld child + " is " + age
  25. 25. Classes, Inheritance and super class Animal constructor: (@name) -> move: (meters) -> alert @name + " moved " + meters + "m." class Snake extends Animal move: -> alert "Slithering..." super 5 class Horse extends Animal move: -> alert "Galloping..." super 45 sam = new Snake "Sammy the Python" tom = new Horse "Tommy the Palomino" sam.move() tom.move()
  26. 26. Function BindingAccount = (customer, cart) -> @customer = customer @cart = cart $(.shopping_cart).bind click, (event) => @customer.purchase @cart
  27. 27. And many othernice features...
  28. 28. How to install?brew install node # or install node.js otherwisecurl http://npmjs.org/install.sh | shnpm install -g coffee-script
  29. 29. Back to theProblem #1
  30. 30. Dynamic single page application
  31. 31. Identifyingcomponents AppView
  32. 32. Identifyingcomponents AppView TodoView TodoView TodoView
  33. 33. Identifyingcomponents AppViewkeypress eventclick event TodoView dblclick event TodoView TodoView click event
  34. 34. Browser-side Views and Models AppView TodoListkeypress eventclick event TodoView Todo dblclick event TodoView Todo TodoView Todo click event
  35. 35. Browser-side Views and Models AppView new, fetch TodoListkeypress eventclick event TodoView create, save Todo dblclick event TodoView Todo TodoView Todo click event
  36. 36. Browser-side Views and Models AppView refresh, add TodoListkeypress eventclick event TodoView Todo dblclick event TodoViewchange, destroy Todo TodoView Todo click event
  37. 37. Browser-side Modelsand RESTful resourcesBrowser Rails GET TodoList TodosController POST index Todo PUT show Todo create DELETE update Todo destroy JSON
  38. 38. Organize CoffeeScriptand JavaScript Code using http://github.com/Sutto/barista
  39. 39. application.coffee # main namespace window.TodoApp = {}
  40. 40. Todo modelclass TodoApp.Todo extends Backbone.Model # If you dont provide a todo, one will be provided for you. EMPTY: "empty todo..." # Ensure that each todo created has `content`. initialize: -> unless @get "content" @set content: @EMPTY # Toggle the `done` state of this todo item. toggle: -> @save done: not @get "done"
  41. 41. TodoList collectionclass TodoApp.TodoList extends Backbone.Collection # Reference to this collections model. model: TodoApp.Todo # Save all of the todo items under the `"todos"` namespace. url: /todos # Filter down the list of all todo items that are finished. done: -> @filter (todo) -> todo.get done # Filter down the list to only todo items that are still not finished. remaining: -> @without this.done()... # We keep the Todos in sequential order, despite being saved by unordered # GUID in the database. This generates the next order number for new items. nextOrder: -> if @length then @last().get(order) + 1 else 1 # Todos are sorted by their original insertion order. comparator: (todo) -> todo.get order
  42. 42. Todo item viewclass TodoApp.TodoView extends Backbone.View # ... is a list tag. tagName: "li" # Cache the template function for a single item. template: TodoApp.template #item-template # The DOM events specific to an item. events: "click .check" : "toggleDone" "dblclick div.todo-content" : "edit" "click span.todo-destroy" : "destroy" "keypress .todo-input" : "updateOnEnter" # The TodoView listens for changes to its model, re-rendering. Since theres # a one-to-one correspondence between a **Todo** and a **TodoView** in this # app, we set a direct reference on the model for convenience. initialize: -> _.bindAll this, render, close @model.bind change, @render @model.bind destroy, => @remove() # Re-render the contents of the todo item. render: -> $(@el).html @template @model.toJSON() @setContent() this
  43. 43. # Re-render the contents of the todo item.render: -> $(@el).html @template @model.toJSON() @setContent() Todo item view this# To avoid XSS (not that it would be harmful in this particular app),# we use `jQuery.text` to set the contents of the todo item.setContent: -> content = @model.get content @$(.todo-content).text content @input = @$(.todo-input) @input.blur @close @input.val content# Toggle the `"done"` state of the model.toggleDone: -> @model.toggle()# Switch this view into `"editing"` mode, displaying the input field.edit: -> $(@el).addClass "editing" @input.focus()# Close the `"editing"` mode, saving changes to the todo.close: -> @model.save content: @input.val() $(@el).removeClass "editing"# If you hit `enter`, were through editing the item.updateOnEnter: (e) -> @close() if e.keyCode == 13# Destroy the model.destroy: -> @model.destroy()
  44. 44. Application viewclass TodoApp.AppView extends Backbone.View # Instead of generating a new element, bind to the existing skeleton of # the App already present in the HTML. el: "#todoapp" # Our template for the line of statistics at the bottom of the app. statsTemplate: TodoApp.template #stats-template # Delegated events for creating new items, and clearing completed ones. events: "keypress #new-todo" : "createOnEnter" "keyup #new-todo" : "showTooltip" "click .todo-clear a" : "clearCompleted" # At initialization we bind to the relevant events on the `Todos` # collection, when items are added or changed. Kick things off by # loading any preexisting todos that might be saved. initialize: -> _.bindAll this, addOne, addAll, renderStats @input = @$("#new-todo") @collection.bind add, @addOne @collection.bind refresh, @addAll @collection.bind all, @renderStats @collection.fetch()
  45. 45. @collection.bind add, @addOne @collection.bind refresh, @addAll @collection.bind all, @renderStats Application view @collection.fetch()# Re-rendering the App just means refreshing the statistics -- the rest# of the app doesnt change.renderStats: -> @$(#todo-stats).html @statsTemplate total: @collection.length done: @collection.done().length remaining: @collection.remaining().length# Add a single todo item to the list by creating a view for it, and# appending its element to the `<ul>`.addOne: (todo) -> view = new TodoApp.TodoView model: todo @$("#todo-list").append view.render().el# Add all items in the collection at once.addAll: -> @collection.each @addOne# Generate the attributes for a new Todo item.newAttributes: -> content: @input.val() order: @collection.nextOrder() done: false# If you hit return in the main input field, create new **Todo** model,# persisting it to server.createOnEnter: (e) -> if e.keyCode == 13 @collection.create @newAttributes() @input.val
  46. 46. view = new TodoApp.TodoView model: todo @$("#todo-list").append view.render().el# Add all items in the collection at once. Application viewaddAll: -> @collection.each @addOne# Generate the attributes for a new Todo item.newAttributes: -> content: @input.val() order: @collection.nextOrder() done: false# If you hit return in the main input field, create new **Todo** model,# persisting it to server.createOnEnter: (e) -> if e.keyCode == 13 @collection.create @newAttributes() @input.val # Clear all done todo items, destroying their views and models.clearCompleted: -> todo.destroy() for todo in @collection.done() false# Lazily show the tooltip that tells you to press `enter` to save# a new todo item, after one second.showTooltip: (e) -> tooltip = @$(".ui-tooltip-top") val = @input.val() tooltip.fadeOut() clearTimeout @tooltipTimeout if @tooltipTimeout unless val == or val == @input.attr placeholder @tooltipTimeout = _.delay -> tooltip.show().fadeIn() , 1000
  47. 47. #todoapp index.html.haml .title %h1 Todos .content #create-todo %input#new-todo{:placeholder => "What needs to be done?", :type => "text"}/ %span.ui-tooltip-top{:style => "display:none;"} Press Enter to save this task #todos %ul#todo-list #todo-stats%ul#instructions %li Double-click to edit a todo.:coffeescript $ -> TodoApp.appView = new TodoApp.AppView collection: new TodoApp.TodoList%script#item-template{:type => "text/html"} .todo{:class => "{{#done}}done{{/done}}"} .display %input{:class => "check", :type => "checkbox", :"{{#done}}checked{{/done}}" => true} .todo-content %span.todo-destroy .edit %input.todo-input{:type => "text", :value => ""}%script#stats-template{:type => "text/html"} {{#if total}} %span.todo-count
  48. 48. %input#new-todo{:placeholder => "What needs to be done?", :type => "text"}/ %span.ui-tooltip-top{:style => "display:none;"} Press Enter to save this task #todos %ul#todo-list index.html.haml #todo-stats%ul#instructions %li Double-click to edit a todo.:coffeescript $ -> TodoApp.appView = new TodoApp.AppView collection: new TodoApp.TodoList%script#item-template{:type => "text/html"} .todo{:class => "{{#done}}done{{/done}}"} .display %input{:class => "check", :type => "checkbox", :"{{#done}}checked{{/done}}" => true} .todo-content %span.todo-destroy .edit %input.todo-input{:type => "text", :value => ""}%script#stats-template{:type => "text/html"} {{#if total}} %span.todo-count %span.number {{remaining}} %span.word {{pluralize remaining "item"}} left. {{/if}} {{#if done}} %span.todo-clear %a{:href => "#"} Clear %span.number-done {{done}} completed %span.word-done {{pluralize done "item"}} {{/if}}
  49. 49. One more thing:Backbone Controllers Routersclass Workspace extends Backbone.Controller Router routes: "help" : "help" #help "search/:query" : "search" #search/kiwis "search/:query/p:page": "search" #search/kiwis/p7 help: -> ... search: (query, page) -> ...
  50. 50. How do you test it?
  51. 51. RSpec-like testing for JavaScript
  52. 52. Together with all other tests
  53. 53. Testing Todo modeldescribe "Todo", -> todo = null ajaxCall = (param) -> jQuery.ajax.mostRecentCall.args[0][param] beforeEach -> todo = new TodoApp.Todo todos = new TodoApp.TodoList [todo] it "should initialize with empty content", -> expect(todo.get "content").toEqual "empty todo..." it "should initialize as not done", -> expect(todo.get "done").toBeFalsy() it "should save after toggle", -> spyOn jQuery, "ajax" todo.toggle() expect(ajaxCall "url").toEqual "/todos" expect(todo.get "done").toBeTruthy()
  54. 54. and TodoList collectiondescribe "TodoList", -> attributes = [ content: "First" done: true , content: "Second" ] todos = null beforeEach -> todos = new TodoApp.TodoList attributes it "should return done todos", -> expect(_.invoke todos.done(), "toJSON").toEqual [attributes[0]] it "should return remaining todos", -> expect(_.invoke todos.remaining(), "toJSON").toEqual [attributes[1]]
  55. 55. Rails 3.1Asset Pipeline
  56. 56. application.js.coffee using Sprockets #= require jquery #= require underscore #= require backbone #= require handlebars #= require ./todo_app #= require_tree ./models #= require ./views/helpers #= require_tree ./views
  57. 57. Watch RailsConf DHH keynotehttp://en.oreilly.com/rails2011/public/schedule/detail/19068
  58. 58. References http://jashkenas.github.com/coffee-script/ http://documentcloud.github.com/backbone/ http://pivotal.github.com/jasmine/https://github.com/rsim/backbone_coffeescript_demo
  59. 59. Used in eazybi.com
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×