• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
 

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

on

  • 25,019 views

Presented at RailsWayCon 2011

Presented at RailsWayCon 2011

Statistics

Views

Total Views
25,019
Views on SlideShare
15,635
Embed Views
9,384

Actions

Likes
40
Downloads
262
Comments
2

24 Embeds 9,384

http://blog.rayapps.com 9117
http://localhost:4000 71
http://localhost 45
http://francescoagati.posterous.com 43
http://paper.li 26
http://translate.googleusercontent.com 13
http://translate.googleusercontent.com 13
http://www.tweetdeck.com 8
url_unknown 8
http://zootool.com 7
http://www.lifeyun.com 6
https://twitter.com 5
http://twitter.com 4
http://feeds.feedburner.com 4
http://www.slideshare.net 3
http://webcache.googleusercontent.com 3
http://www.newsblur.com 1
http://seekr-artemis.heroku.com 1
http://www.365dailyjournal.com 1
http://honyaku.yahoofs.jp 1
http://www.hanrss.com 1
http://a0.twimg.com 1
http://static.slidesharecdn.com 1
https://www.google.com 1
More...

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel

12 of 2 previous next

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

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

    • 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 included by javascript_include_tag :defaults
    • Which leads to...(example from Redmine)
    • 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); }
    • /* * 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);
    • The Problem #2
    • Do we really know (and love?) JavaScript?
    • 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
    • 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
    • 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
    • “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
    • 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
    • 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
    • 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
    • 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...
    • 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
    • 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();
    • Existential Operator solipsism = true if mind? and not world? speed ?= 75 footprints = yeti ? "bear" zip = lottery.drawWinner?().address?.zipcode
    • Conditionalsmood = greatlyImproved if singingif happy and knowsIt clapsHands() chaChaCha()else showIt()date = if friday then sue else jilloptions or= defaults
    • 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
    • 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()
    • Function BindingAccount = (customer, cart) -> @customer = customer @cart = cart $(.shopping_cart).bind click, (event) => @customer.purchase @cart
    • And many othernice features...
    • How to install?brew install node # or install node.js otherwisecurl http://npmjs.org/install.sh | shnpm install -g coffee-script
    • Back to theProblem #1
    • Dynamic single page application
    • Identifyingcomponents AppView
    • Identifyingcomponents AppView TodoView TodoView TodoView
    • Identifyingcomponents AppViewkeypress eventclick event TodoView dblclick event TodoView TodoView click event
    • Browser-side Views and Models AppView TodoListkeypress eventclick event TodoView Todo dblclick event TodoView Todo TodoView Todo click event
    • Browser-side Views and Models AppView new, fetch TodoListkeypress eventclick event TodoView create, save Todo dblclick event TodoView Todo TodoView Todo click event
    • Browser-side Views and Models AppView refresh, add TodoListkeypress eventclick event TodoView Todo dblclick event TodoViewchange, destroy Todo TodoView Todo click event
    • Browser-side Modelsand RESTful resourcesBrowser Rails GET TodoList TodosController POST index Todo PUT show Todo create DELETE update Todo destroy JSON
    • 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: "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"
    • 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
    • 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
    • # 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()
    • 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()
    • @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
    • 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
    • #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
    • %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}}
    • 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) -> ...
    • 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] 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()
    • 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]]
    • Rails 3.1Asset Pipeline
    • 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
    • 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://pivotal.github.com/jasmine/https://github.com/rsim/backbone_coffeescript_demo
    • Used in eazybi.com