6. application.js
// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults
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')); // true
console.log(CreditCard.validNumber('4111111111111121')); // false
13. 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')); # true
puts(CreditCard[:validNumber].call('4111111111111121')); # false
14. Or as this “normal” Ruby
module 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
end
end
puts CreditCard.valid_number?('4111 1111-11111111') # true
puts CreditCard.valid_number?('4111111111111121') # false
15. “Best practices” Ruby
class 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(/[- ]/, "")
end
end
puts CreditCard.new('4111 1111-11111111').valid? # true
puts CreditCard.new('4111111111111121').valid? # false
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() ); // true
console.log( (new CreditCard('4111111111111121')).isValid() ); // false
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 == 0
console.log (new CreditCard '4111 1111-11111111').isValid() # true
console.log (new CreditCard '4111111111111121').isValid() # false
20. 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
21. Functions
square = (x) -> x * x
cube = (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...
24. Existential Operator
solipsism = true if mind? and not world?
speed ?= 75
footprints = yeti ? "bear"
zip = lottery.drawWinner?().address?.zipcode
25. Conditionals
mood = greatlyImproved if singing
if happy and knowsIt
clapsHands()
chaChaCha()
else
showIt()
date = if friday then sue else jill
options or= defaults
26. Loops
eat food for food in ['toast', 'cheese', 'wine']
countdown = (num for num in [10..1])
earsOld = max: 10, ida: 9, tim: 11
ages = for child, age of yearsOld
child + " is " + age
27. 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()
36. Browser-side
Views and Models
AppView
TodoList
keypress event
click event TodoView Todo
dblclick event TodoView Todo
TodoView Todo
click event
37. Browser-side
Views and Models
AppView
new, fetch
TodoList
keypress event
click event TodoView create, save
Todo
dblclick event TodoView Todo
TodoView Todo
click event
38. Browser-side
Views and Models
AppView
refresh, add TodoList
keypress event
click event TodoView Todo
dblclick event TodoViewchange, destroy Todo
TodoView Todo
click event
39. Browser-side Models
and RESTful resources
Browser Rails
GET
TodoList TodosController
POST index
Todo PUT show
Todo create
DELETE update
Todo
destroy
JSON
43. Todo model
class 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"
44. TodoList collection
class 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'
45. 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
46. # 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`, we're through editing the item.
updateOnEnter: (e) ->
@close() if e.keyCode == 13
# Destroy the model.
destroy: ->
@model.destroy()
47. 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()
48. @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 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 ''
49. view = new TodoApp.TodoView model: todo
@$("#todo-list").append view.render().el
# Add all items in the collection at once.
Application view
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
50. #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
51. %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}}
56. Testing Todo model
describe "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()
57. 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]]