rails-like javascript using coffeescript, backbone.js and jasmine
DESCRIPTION
Presented at RailsWayCon 2011TRANSCRIPT
![Page 1: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/1.jpg)
Rails-like JavaScript using CoffeeScript, Backbone.js and Jasmine
Raimonds Simanovskis
![Page 2: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/2.jpg)
Raimonds Simanovskis
github.com/rsim
@rsim
.com
![Page 3: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/3.jpg)
The Problem
![Page 4: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/4.jpg)
Ruby code in Rails
![Page 5: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/5.jpg)
JavaScript code in Rails 3.0.x
![Page 6: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/6.jpg)
application.js// Place your application-specific JavaScript functions and classes here// This file is automatically included by javascript_include_tag :defaults
![Page 7: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/7.jpg)
Which leads to...(example from Redmine)
![Page 8: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/8.jpg)
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); } Element.scrollTo(id);}
function toggleRowGroup(el) { var tr = Element.up(el, 'tr'); var n = Element.next(tr); tr.toggleClassName('open'); while (n != undefined && !n.hasClassName('group')) { Element.toggle(n); n = Element.next(n); }}
function collapseAllRowGroups(el) { var tbody = Element.up(el, 'tbody'); tbody.childElements('tr').each(function(tr) { if (tr.hasClassName('group')) { tr.removeClassName('open'); } else { tr.hide(); } })}
function expandAllRowGroups(el) { var tbody = Element.up(el, 'tbody'); tbody.childElements('tr').each(function(tr) { if (tr.hasClassName('group')) { tr.addClassName('open'); } else { tr.show(); } })}
function toggleAllRowGroups(el) { var tr = Element.up(el, 'tr'); if (tr.hasClassName('open')) { collapseAllRowGroups(el); } else { expandAllRowGroups(el); }}
function toggleFieldset(el) { var fieldset = Element.up(el, 'fieldset'); fieldset.toggleClassName('collapsed'); Effect.toggle(fieldset.down('div'), 'slide', {duration:0.2});}
function hideFieldset(el) { var fieldset = Element.up(el, 'fieldset'); fieldset.toggleClassName('collapsed'); fieldset.down('div').hide();}
var fileFieldCount = 1;
function addFileField() { if (fileFieldCount >= 10) return false fileFieldCount++; var f = document.createElement("input"); f.type = "file"; f.name = "attachments[" + fileFieldCount + "][file]"; f.size = 30; var d = document.createElement("input"); d.type = "text"; d.name = "attachments[" + fileFieldCount + "][description]"; d.size = 60; var dLabel = new Element('label'); dLabel.addClassName('inline'); // Pulls the languge value used for Optional Description dLabel.update($('attachment_description_label_content').innerHTML) p = document.getElementById("attachments_fields"); p.appendChild(document.createElement("br")); p.appendChild(f); p.appendChild(dLabel); dLabel.appendChild(d);
}
function showTab(name) { var f = $$('div#content .tab-content'); for(var i=0; i<f.length; i++){ Element.hide(f[i]); } var f = $$('div.tabs a'); for(var i=0; i<f.length; i++){ Element.removeClassName(f[i], "selected"); } Element.show('tab-content-' + name); Element.addClassName('tab-' + name, "selected"); return false;}
function moveTabRight(el) { var lis = Element.up(el, 'div.tabs').down('ul').childElements(); var tabsWidth = 0; var i; for (i=0; i<lis.length; i++) { if (lis[i].visible()) { tabsWidth += lis[i].getWidth() + 6; } } if (tabsWidth < Element.up(el, 'div.tabs').getWidth() - 60) { return; } i=0; while (i<lis.length && !lis[i].visible()) { i++; } lis[i].hide();}
function moveTabLeft(el) { var lis = Element.up(el, 'div.tabs').down('ul').childElements(); var i = 0; while (i<lis.length && !lis[i].visible()) { i++; } if (i>0) { lis[i-1].show(); }}
function displayTabsButtons() { var lis; var tabsWidth = 0; var i; $$('div.tabs').each(function(el) { lis = el.down('ul').childElements(); for (i=0; i<lis.length; i++) { if (lis[i].visible()) { tabsWidth += lis[i].getWidth() + 6; } } if ((tabsWidth < el.getWidth() - 60) && (lis[0].visible())) { el.down('div.tabs-buttons').hide(); } else { el.down('div.tabs-buttons').show(); } });}
function setPredecessorFieldsVisibility() { relationType = $('relation_relation_type'); if (relationType && (relationType.value == "precedes" || relationType.value == "follows")) { Element.show('predecessor_fields'); } else { Element.hide('predecessor_fields'); }}
function promptToRemote(text, param, url) { value = prompt(text + ':'); if (value) { new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true}); return false; }}
function collapseScmEntry(id) { var els = document.getElementsByClassName(id, 'browser'); for (var i = 0; i < els.length; i++) { if (els[i].hasClassName('open')) { collapseScmEntry(els[i].id); } Element.hide(els[i]); } $(id).removeClassName('open');}
function expandScmEntry(id) { var els = document.getElementsByClassName(id, 'browser'); for (var i = 0; i < els.length; i++) { Element.show(els[i]); if (els[i].hasClassName('loaded') && !els[i].hasClassName('collapsed')) { expandScmEntry(els[i].id); } } $(id).addClassName('open');}
function scmEntryClick(id) { el = $(id); if (el.hasClassName('open')) { collapseScmEntry(id); el.addClassName('collapsed'); return false; } else if (el.hasClassName('loaded')) { expandScmEntry(id); el.removeClassName('collapsed'); return false; } if (el.hasClassName('loading')) { return false; } el.addClassName('loading'); return true;}
function scmEntryLoaded(id) { Element.addClassName(id, 'open'); Element.addClassName(id, 'loaded'); Element.removeClassName(id, 'loading');}
function randomKey(size) { var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'); var key = ''; for (i = 0; i < size; i++) { key += chars[Math.floor(Math.random() * chars.length)]; } return key;}
function observeParentIssueField(url) { new Ajax.Autocompleter('issue_parent_issue_id', 'parent_issue_candidates', url, { minChars: 3, frequency: 0.5, paramName: 'q', updateElement: function(value) { document.getElementById('issue_parent_issue_id').value = value.id; }});}
function observeRelatedIssueField(url) { new Ajax.Autocompleter('relation_issue_to_id', 'related_issue_candidates', url, { minChars: 3, frequency: 0.5, paramName: 'q', updateElement: function(value) { document.getElementById('relation_issue_to_id').value = value.id; }, parameters: 'scope=all' });}
function setVisible(id, visible) { var el = $(id); if (el) {if (visible) {el.show();} else {el.hide();}}}
function observeProjectModules() { var f = function() { /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */ var c = ($('project_enabled_module_names_issue_tracking').checked == true); setVisible('project_trackers', c); setVisible('project_issue_custom_fields', c); }; Event.observe(window, 'load', f); Event.observe('project_enabled_module_names_issue_tracking', 'change', f);}
/* * Class used to warn user when leaving a page with unsaved textarea * Author: [email protected]*/
var WarnLeavingUnsaved = Class.create({ observedForms: false, observedElements: false, changedForms: false, message: null, initialize: function(message){ this.observedForms = $$('form'); this.observedElements = $$('textarea'); this.message = message; this.observedElements.each(this.observeChange.bind(this)); this.observedForms.each(this.submitAction.bind(this)); window.onbeforeunload = this.unload.bind(this); }, unload: function(){ if(this.changedForms) return this.message; }, setChanged: function(){ this.changedForms = true; }, setUnchanged: function(){ this.changedForms = false; }, observeChange: function(element){ element.observe('change',this.setChanged.bindAsEventListener(this)); }, submitAction: function(element){ element.observe('submit',this.setUnchanged.bindAsEventListener(this)); }});
/* * 1 - registers a callback which copies the csrf token into the * X-CSRF-Token header with each ajax request. Necessary to * work with rails applications which have fixed * CVE-2011-0447 * 2 - shows and hides ajax indicator */Ajax.Responders.register({ 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);
![Page 9: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/9.jpg)
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); } Element.scrollTo(id);}
function toggleRowGroup(el) { var tr = Element.up(el, 'tr'); var n = Element.next(tr); tr.toggleClassName('open'); while (n != undefined && !n.hasClassName('group')) { Element.toggle(n); n = Element.next(n); }}
function collapseAllRowGroups(el) { var tbody = Element.up(el, 'tbody'); tbody.childElements('tr').each(function(tr) { if (tr.hasClassName('group')) { tr.removeClassName('open'); } else { tr.hide(); } })}
function expandAllRowGroups(el) { var tbody = Element.up(el, 'tbody'); tbody.childElements('tr').each(function(tr) { if (tr.hasClassName('group')) { tr.addClassName('open'); } else { tr.show(); } })}
function toggleAllRowGroups(el) { var tr = Element.up(el, 'tr'); if (tr.hasClassName('open')) { collapseAllRowGroups(el); } else { expandAllRowGroups(el); }}
function toggleFieldset(el) { var fieldset = Element.up(el, 'fieldset'); fieldset.toggleClassName('collapsed'); Effect.toggle(fieldset.down('div'), 'slide', {duration:0.2});}
function hideFieldset(el) { var fieldset = Element.up(el, 'fieldset'); fieldset.toggleClassName('collapsed'); fieldset.down('div').hide();}
var fileFieldCount = 1;
function addFileField() { if (fileFieldCount >= 10) return false fileFieldCount++; var f = document.createElement("input"); f.type = "file"; f.name = "attachments[" + fileFieldCount + "][file]"; f.size = 30; var d = document.createElement("input"); d.type = "text"; d.name = "attachments[" + fileFieldCount + "][description]"; d.size = 60; var dLabel = new Element('label'); dLabel.addClassName('inline'); // Pulls the languge value used for Optional Description dLabel.update($('attachment_description_label_content').innerHTML) p = document.getElementById("attachments_fields"); p.appendChild(document.createElement("br")); p.appendChild(f); p.appendChild(dLabel); dLabel.appendChild(d);
}
function showTab(name) { var f = $$('div#content .tab-content'); for(var i=0; i<f.length; i++){ Element.hide(f[i]); } var f = $$('div.tabs a'); for(var i=0; i<f.length; i++){ Element.removeClassName(f[i], "selected"); } Element.show('tab-content-' + name); Element.addClassName('tab-' + name, "selected"); return false;}
function moveTabRight(el) { var lis = Element.up(el, 'div.tabs').down('ul').childElements(); var tabsWidth = 0; var i; for (i=0; i<lis.length; i++) { if (lis[i].visible()) { tabsWidth += lis[i].getWidth() + 6; } } if (tabsWidth < Element.up(el, 'div.tabs').getWidth() - 60) { return; } i=0; while (i<lis.length && !lis[i].visible()) { i++; } lis[i].hide();}
function moveTabLeft(el) { var lis = Element.up(el, 'div.tabs').down('ul').childElements(); var i = 0; while (i<lis.length && !lis[i].visible()) { i++; } if (i>0) { lis[i-1].show(); }}
function displayTabsButtons() { var lis; var tabsWidth = 0; var i; $$('div.tabs').each(function(el) { lis = el.down('ul').childElements(); for (i=0; i<lis.length; i++) { if (lis[i].visible()) { tabsWidth += lis[i].getWidth() + 6; } } if ((tabsWidth < el.getWidth() - 60) && (lis[0].visible())) { el.down('div.tabs-buttons').hide(); } else { el.down('div.tabs-buttons').show(); } });}
function setPredecessorFieldsVisibility() { relationType = $('relation_relation_type'); if (relationType && (relationType.value == "precedes" || relationType.value == "follows")) { Element.show('predecessor_fields'); } else { Element.hide('predecessor_fields'); }}
function promptToRemote(text, param, url) { value = prompt(text + ':'); if (value) { new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true}); return false; }}
function collapseScmEntry(id) { var els = document.getElementsByClassName(id, 'browser'); for (var i = 0; i < els.length; i++) { if (els[i].hasClassName('open')) { collapseScmEntry(els[i].id); } Element.hide(els[i]); } $(id).removeClassName('open');}
function expandScmEntry(id) { var els = document.getElementsByClassName(id, 'browser'); for (var i = 0; i < els.length; i++) { Element.show(els[i]); if (els[i].hasClassName('loaded') && !els[i].hasClassName('collapsed')) { expandScmEntry(els[i].id); } } $(id).addClassName('open');}
function scmEntryClick(id) { el = $(id); if (el.hasClassName('open')) { collapseScmEntry(id); el.addClassName('collapsed'); return false; } else if (el.hasClassName('loaded')) { expandScmEntry(id); el.removeClassName('collapsed'); return false; } if (el.hasClassName('loading')) { return false; } el.addClassName('loading'); return true;}
function scmEntryLoaded(id) { Element.addClassName(id, 'open'); Element.addClassName(id, 'loaded'); Element.removeClassName(id, 'loading');}
function randomKey(size) { var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'); var key = ''; for (i = 0; i < size; i++) { key += chars[Math.floor(Math.random() * chars.length)]; } return key;}
function observeParentIssueField(url) { new Ajax.Autocompleter('issue_parent_issue_id', 'parent_issue_candidates', url, { minChars: 3, frequency: 0.5, paramName: 'q', updateElement: function(value) { document.getElementById('issue_parent_issue_id').value = value.id; }});}
function observeRelatedIssueField(url) { new Ajax.Autocompleter('relation_issue_to_id', 'related_issue_candidates', url, { minChars: 3, frequency: 0.5, paramName: 'q', updateElement: function(value) { document.getElementById('relation_issue_to_id').value = value.id; }, parameters: 'scope=all' });}
function setVisible(id, visible) { var el = $(id); if (el) {if (visible) {el.show();} else {el.hide();}}}
function observeProjectModules() { var f = function() { /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */ var c = ($('project_enabled_module_names_issue_tracking').checked == true); setVisible('project_trackers', c); setVisible('project_issue_custom_fields', c); }; Event.observe(window, 'load', f); Event.observe('project_enabled_module_names_issue_tracking', 'change', f);}
/* * Class used to warn user when leaving a page with unsaved textarea * Author: [email protected]*/
var WarnLeavingUnsaved = Class.create({ observedForms: false, observedElements: false, changedForms: false, message: null, initialize: function(message){ this.observedForms = $$('form'); this.observedElements = $$('textarea'); this.message = message; this.observedElements.each(this.observeChange.bind(this)); this.observedForms.each(this.submitAction.bind(this)); window.onbeforeunload = this.unload.bind(this); }, unload: function(){ if(this.changedForms) return this.message; }, setChanged: function(){ this.changedForms = true; }, setUnchanged: function(){ this.changedForms = false; }, observeChange: function(element){ element.observe('change',this.setChanged.bindAsEventListener(this)); }, submitAction: function(element){ element.observe('submit',this.setUnchanged.bindAsEventListener(this)); }});
/* * 1 - registers a callback which copies the csrf token into the * X-CSRF-Token header with each ajax request. Necessary to * work with rails applications which have fixed * CVE-2011-0447 * 2 - shows and hides ajax indicator */Ajax.Responders.register({ 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);
![Page 10: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/10.jpg)
The Problem #2
![Page 11: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/11.jpg)
Do we really know(and love?)
JavaScript?
![Page 12: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/12.jpg)
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
![Page 13: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/13.jpg)
We see as this“ugly” Ruby
CreditCard = { :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
![Page 14: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/14.jpg)
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 endend
puts CreditCard.valid_number?('4111 1111-11111111') # trueputs CreditCard.valid_number?('4111111111111121') # false
![Page 15: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/15.jpg)
“Best practices” Rubyclass CreditCard def initialize(number) @number = clean_number(number) end
def valid? total = 0 for i in [email protected] n = @number[i].to_i if [email protected] % 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(/[- ]/, "") endend
puts CreditCard.new('4111 1111-11111111').valid? # trueputs CreditCard.new('4111111111111121').valid? # false
![Page 16: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/16.jpg)
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
![Page 17: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/17.jpg)
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 [[email protected]] n = +@number[i] if ([email protected]) % 2 == 0 n = if n*2 > 9 then n*2 - 9 else n*2 total += n total % 10 == 0
console.log (new CreditCard '4111 1111-11111111').isValid() # trueconsole.log (new CreditCard '4111111111111121').isValid() # false
![Page 18: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/18.jpg)
![Page 19: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/19.jpg)
![Page 20: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/20.jpg)
Sample CoffeeScript# Assignment:number = 42opposite = true
# Conditions:number = -42 if opposite
# Functions:square = (x) -> x * x
# Arrays:list = [1, 2, 3, 4, 5]
# Objects:math = root: Math.sqrt square: square cube: (x) -> x * square x
# Splats:race = (winner, runners...) -> print winner, runners
# Existence:alert "I knew it!" if elvis?
# Array comprehensions:cubes = (math.cube num for num in list)
![Page 21: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/21.jpg)
Functionssquare = (x) -> x * xcube = (x) -> square(x) * x
fill = (container, liquid = "coffee") -> "Filling the #{container} with #{liquid}..."
awardMedals = (first, second, others...) -> gold = first silver = second rest = others
contenders = [ "Michael Phelps" "Liu Xiang" "Yao Ming" "Allyson Felix" "Shawn Johnson"]
awardMedals contenders...
![Page 22: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/22.jpg)
Objects and Arrayssong = ["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
![Page 23: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/23.jpg)
Variable Scope
outer = 1changeNumbers = -> inner = -1 outer = 10inner = changeNumbers()
var changeNumbers, inner, outer;outer = 1;changeNumbers = function() { var inner; inner = -1; return outer = 10;};inner = changeNumbers();
![Page 24: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/24.jpg)
Existential Operator
solipsism = true if mind? and not world?
speed ?= 75
footprints = yeti ? "bear"
zip = lottery.drawWinner?().address?.zipcode
![Page 25: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/25.jpg)
Conditionals
mood = greatlyImproved if singing
if happy and knowsIt clapsHands() chaChaCha()else showIt()
date = if friday then sue else jill
options or= defaults
![Page 26: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/26.jpg)
Loops
eat 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
![Page 27: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/27.jpg)
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()
![Page 28: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/28.jpg)
Function Binding
Account = (customer, cart) -> @customer = customer @cart = cart
$('.shopping_cart').bind 'click', (event) => @customer.purchase @cart
![Page 29: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/29.jpg)
And many othernice features...
![Page 30: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/30.jpg)
How to install?
brew install node # or install node.js otherwisecurl http://npmjs.org/install.sh | shnpm install -g coffee-script
![Page 31: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/31.jpg)
Back to theProblem #1
![Page 32: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/32.jpg)
Dynamic single page application
![Page 33: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/33.jpg)
AppView
Identifying components
![Page 34: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/34.jpg)
AppView
Identifying components
TodoViewTodoViewTodoView
![Page 35: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/35.jpg)
AppView
Identifying components
TodoViewTodoViewTodoView
keypress event
click event
click eventdblclick event
![Page 36: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/36.jpg)
TodoList
AppView
Browser-sideViews and Models
TodoViewTodoViewTodoView
keypress event
click event
click eventdblclick event
TodoTodoTodo
![Page 37: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/37.jpg)
TodoList
AppView
Browser-sideViews and Models
TodoViewTodoViewTodoView
keypress event
click event
click eventdblclick event
TodoTodoTodo
new, fetch
create, save
![Page 38: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/38.jpg)
TodoList
AppView
Browser-sideViews and Models
TodoViewTodoViewTodoView
keypress event
click event
click eventdblclick event
TodoTodoTodo
refresh, add
change, destroy
![Page 39: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/39.jpg)
TodoList
Browser-side Models and RESTful resources
TodoTodoTodo
TodosControllerindexshowcreateupdatedestroy
GET
JSON
POST
PUT
DELETE
Browser Rails
![Page 40: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/40.jpg)
![Page 41: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/41.jpg)
Organize CoffeeScript and JavaScript Code
using http://github.com/Sutto/barista
![Page 42: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/42.jpg)
application.coffee
# main namespacewindow.TodoApp = {}
![Page 43: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/43.jpg)
Todo modelclass TodoApp.Todo extends Backbone.Model
# If you don't 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"
![Page 44: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/44.jpg)
TodoList collectionclass TodoApp.TodoList extends Backbone.Collection
# Reference to this collection's 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'
![Page 45: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/45.jpg)
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 there's # 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
# 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`, we're through editing the item. updateOnEnter: (e) -> @close() if e.keyCode == 13
# Destroy the model. destroy: -> @model.destroy()
![Page 46: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/46.jpg)
Todo item view
class 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 there's # 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
# 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`, we're through editing the item. updateOnEnter: (e) -> @close() if e.keyCode == 13
# Destroy the model. destroy: -> @model.destroy()
![Page 47: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/47.jpg)
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()
# Re-rendering the App just means refreshing the statistics -- the rest # of the app doesn't 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 ''
# 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
![Page 48: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/48.jpg)
Application view
class 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()
# Re-rendering the App just means refreshing the statistics -- the rest # of the app doesn't 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 ''
# 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
![Page 49: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/49.jpg)
Application view
class 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()
# Re-rendering the App just means refreshing the statistics -- the rest # of the app doesn't 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 ''
# 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
![Page 50: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/50.jpg)
index.html.haml#todoapp .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 %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}}
![Page 51: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/51.jpg)
index.html.haml
#todoapp .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 %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}}
![Page 52: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/52.jpg)
One more thing:Backbone Controllersclass Workspace extends Backbone.Controller
routes: "help" : "help" #help "search/:query" : "search" #search/kiwis "search/:query/p:page": "search" #search/kiwis/p7
help: -> ...
search: (query, page) -> ...
RoutersRouter
![Page 53: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/53.jpg)
How do you test it?
![Page 54: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/54.jpg)
RSpec-like testing for JavaScript
![Page 55: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/55.jpg)
Together with all other tests
![Page 56: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/56.jpg)
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()
![Page 57: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/57.jpg)
and TodoList collection
describe "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]]
![Page 58: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/58.jpg)
Rails 3.1Asset Pipeline
![Page 59: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/59.jpg)
application.js.coffeeusing Sprockets
#= require jquery#= require underscore#= require backbone#= require handlebars#= require ./todo_app#= require_tree ./models#= require ./views/helpers#= require_tree ./views
![Page 60: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/60.jpg)
Watch RailsConfDHH keynote
http://en.oreilly.com/rails2011/public/schedule/detail/19068
![Page 61: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/61.jpg)
References
http://jashkenas.github.com/coffee-script/
http://documentcloud.github.com/backbone/
http://pivotal.github.com/jasmine/
https://github.com/rsim/backbone_coffeescript_demo
![Page 62: Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine](https://reader034.vdocument.in/reader034/viewer/2022042814/55593038d8b42a4f3d8b48e0/html5/thumbnails/62.jpg)
Used in eazybi.com