Chapter 13
ECMAScript and the future of CoffeeScript
13.1 CoffeeScript in the context of JavaScript
Listing 13.1 Formulaic form bindings
class Formulaic
constructor: (@root, @selector, @http) ->
@subscribers = []
@fields = @extractFields()
@startPolling()
extractFields: ->
element = @root.querySelector @selector
fields = element.getElementsByTagName 'input'
extracted = {}
for field in fields
extracted[field.name] = field.value
extracted
startPolling: ->
diff = =>
for own key, value of @extractFields()
if @fields[key] isnt value
@fields[key] = value
@notify()
setInterval diff, 100
subscribe: (subscriber) ->
@subscribers.push subscriber
notify: ->
subscriber() for subscriber in @subscribers
<!doctype html>
<html>
<form id='contact-details'>
<input type='text' name='first-name'>
<input type='text' name='last-name'>
<input type='text' name='email'>
new Formulaic document, '#contact-details'
rest = (a, b, r...) -> r
rest 1,2,3,4,5,6,7
# [3,4,5,6,7]
spread = [1,2,3,4,5,6,7]
rest spread
# [3,4,5,6,7]
rest = function (a, b, ...r) { return r; }
rest(1,2,3,4,5,6,7);
# [3,4,5,6,7]
word = 'interpolation'
"Just like string #{word}"
# Just like string interpolation
"Just like string " + word
"""
<html>
<title>#{title}</title>
<body>
<h1>#{ heading}</h1>
#{content}
"""
var word = 'interpolation';
`Just like string ${word}.`
# Just like string interpolation.
let square = (x) => x * x
13.2 ECMAScript 5
homer =
'first-name': 'Homer'
'last-name': 'Simpson'
homerTwo = Object.create homer,
clone:
value: true
writable: false
'middle-name':
value: 'Clone'
writable: false
homerTwo.clone
# true
sync: ->
throw new Error 'No transport' unless @http? and @url?
data = extractFields
@http.post @url, JSON.stringify(@)
fred = {firstName: 'Fred', lastName: 'Flintstone'}
JSON.stringify fred
# '{"firstName":"Fred","lastName":"Flintstone"}'
{
"root":{
"location":{},
"contact-form":{
"0":{},
"1":{},
}
},
"selector":".contact-form",
"subscribers":[],
"fields":{
"search":""
}
}
JSON.stringify @
# TypeError: Converting circular structure to JSON
class Formulaic
toJSON: -> "message": "Determine your own JSON representation"
formulaic = new Formulaic
JSON.stringify formulaic
# '{"message":"Determine your own JSON representation"}'
sync: ->
throw new Error 'No transport' unless @http? and @url?
@http.post @url, JSON.stringify(@extractFields()), (response) ->
@fields = JSON.parse response
JSON.parse ?= (json) ->
eval json
barney = JSON.parse '{"firstName":"Barney","lastName":"Rubble"}'"
barney.lastName
# "Rubble"
Listing 13.2 Formulaic with server sync
class Formulaic
constructor: (@root, @selector, @http) ->
@subscribers = []
@fields = @extractFields()
@startPolling()
extractFields: ->
element = @root.querySelector @selector
fields = element.getElementsByTagName 'input'
extracted = {}
for field in fields
extracted[field.name] = field.value
extracted
update: =>
for own key, value of @extractFields()
if @fields[key] isnt value
@fields[key] = value
@notify()
startPolling: ->
setInterval @update, 100
subscribe: (subscriber) ->
@subscribers.push subscriber
notify: ->
subscriber() for subscriber in @subscribers
sync: ->
throw new Error 'No transport' unless @http? and @url?
@http.post @url, JSON.stringify(@extractFields()), (response) ->
@fields = JSON.parse response
exports.Formulaic = Formulaic
form = new Formulaic document, '#contact-details'
form.fields['first-name'] = 'Tyrone'
form.fields =
'first-name': 'Fred'
Object.defineProperty form.fields, 'first-name',
set: (newValue) => @root.getElementsByName('first-name')[0].value = newValue
Object.defineProperty form.fields, 'first-name',
set: (newValue) => @updateView 'first-name', newValue
get: => @updateField 'first-name'
Listing 13.3 Using getters and setters
class Formulaic
"use strict"
constructor: (@root, @selector, @http) ->
@fields = {}
@subscribers = []
@extractFields()
bind: (field) ->
Object.defineProperty @fields, field.name,
set: (newValue) =>
field.value = newValue
@sync()
get: ->
field.value
enumerable: true
updateField = =>
@fields[field.name] = field.value
updateField()
field.addEventListener 'input', updateField
documentFields: ->
element = @root.querySelector @selector
element.getElementsByTagName 'input'
extractFields: ->
@bind field for field in @documentFields()
sync: ->
throw new Error 'No transport' unless @http?
if @url?
@http.post @url, JSON.stringify(@fields), (response) =>
@fields = JSON.parse response
exports.Formulaic = Formulaic
class User
user = Object.create User.prototype,
id:
value: 'u58440329'
enumerable: true
writable: false
configurable: false
name:
value: 'Robert'
user.id = '0'
user.id
# u58440329
property for property of user
# [ 'name' ]
Object.defineProperty user, id, writable: true
# Cannot redefine property: id
user =
name: 'Robert'
Object.freeze user
user.name = 'Janet'
user.name
# 'Robert'
user.address = '10 Elephant Parade'
user.address?
# false
Object.defineProperty user, 'phone', value: '555 4312'
# Cannot define property:phone, object is not extensible.
form = new Formulaic, '#form'
Object.freeze form
form.fields.user.id = 'u2344999'
form.fields.user.id
# 'u2344999'
deepFreeze = (o) ->
Object.freeze o
for own propKey of o
prop = o[propKey]
if typeof prop != 'object' || Object.isFrozen prop
continue
deepFreeze prop
Listing 13.4 Formulaic with disabled fields
class Formulaic
"use strict"
constructor: (@root, @selector, @http) ->
@fields = {}
@extractFields()
bind: (field) ->
Object.defineProperty @fields, field.name,
set: (newValue) =>
field.value = newValue
@sync()
get: ->
field.value
enumerable: true
configurable: true
updateField = =>
@fields[field.name] = field.value
updateField()
field.addEventListener 'input', updateField
disable: ->
for key, value of @fields
Object.defineProperty @fields, key, { value }
for field in @documentFields()
field.disabled = true
Object.freeze @fields
documentFields: ->
element = @root.querySelector @selector
element.getElementsByTagName 'input'
extractFields: ->
@bind field for field in @documentFields()
sync: ->
throw new Error 'No transport' unless @http?
if @url?
@http.post @url, JSON.stringify(@fields), (response) =>
@fields = JSON.parse response
exports.Formulaic = Formulaic
failsStrictMode = function() {
"use strict";
undeclaredVariable = 3;
};
failsStrictMode()
// Reference Error
"use strict"
failsStrict =
duplicateKey: 1
"duplicateKey": 2
13.3 ECMAScript 6
> node --version
> coffee -i --nodejs --harmony-proxies
> coffee -i --nodejs --harmony
module Formulaic
export Formulaic
module QuoteApplication
import { Formulaic } from Formulaic
ONE_SECOND = 1000
setInterval ->
rocket.forward()
, ONE_SECOND
ONE_SECOND = 0
MODE = 'development'
const MODE = 'development';
MODE = 5;
# TypeError: redeclaration of const MODE
number = 4
double = (numbers) -> number * 2 for number in numbers
double [3,4,5,6]
number
# 6
if (something) {
let x = 3;
}
number = 5
do (number=0) -> number for number in numbers
number = 4
double = do (number=0) ->
(numbers) -> number * 2 for number in numbers
double [3,4,5,6]
number
# 4
shopping = new Set()
shopping.has 'eggs'
# false
shopping.add 'milk'
shopping.has 'milk'
# true
shopping.delete 'milk'
shopping.has 'milk'
# false
map = new Map
harold = name: 'Harold'
map.set harold, age: 50
map.get harold
# {age: 50}
map.get {name: 'Harold'}
# undefined
stackOfPapers =
paperOnGladiators:
text:
"Gladiators, it seems, were fat."
papersMap = new WeakMap
papersMap.add stackOfPapers.paperOnGladiators
delete stackOfPapers.paperOnGladiators
class SecretHideout
hideout = new SecretHideout()
proxy = new Proxy hideout, {}
proxy.policeAssault = true
hideout.policeAssault
# false
form = document.getElementById '#the-form'
handler =
get: -> 'property access intercepted by proxy'
set: -> 'property modify intercepted by proxy'
proxiedForm = new Proxy form, handler
proxiedForm.name
# 'property access intercepted by proxy'
proxiedForm.phone = '555 9988'
# 'property modify intercepted by proxy'
Listing 13.5 Formulaic using Proxy
throw new Error 'Proxy required' unless Proxy?
class Formulaic
constructor: (@root, @selector, @http, @url) ->
@source = @root.querySelector @selector
@handler =
get: (target, property) ->
target[property]?.value
set: (target, property, value) =>
if @valid property then @sync()
@fields = new Proxy @source, @handler
valid: (property) ->
property isnt ''
addField: (field, value) ->
throw new Error "Can't append to DOM" unless @source.appendChild?
newField = @root.createElement 'input'
newField.value = value
@source.appendChild newField
sync: ->
throw new Error 'No HTTP specified' unless @http? and @url?
@http.post @url, JSON.stringify(@source), (response) => #B
for field, fieldResponse of JSON.parse response
if field of @source
@source[field].value = fieldResponse.value
else
@addField field, fieldResponse.value
form = new Formulaic document, '#form', http, 'http://agtron.co/formulaic/1'
form.fields.login = 'scruffy1234'
double = function(n) {
return n * 2;
};
numbers = [2,3,5,4,2];
var doubled = [double number for each (number in numbers)];
double (n) ->
n * 2
numbers = [2,3,5,4,2]
doubled = double number for number in numbers
[UP, DOWN, LEFT, LEFT, DOWN]
keyCommandIterator =
next: ->
if Math.floor Math.random()*10 > 5
UP
else
DOWN
keyCommandIterator.next()
# UP
keyCommandIterator.next()
# UP
class KeyboardEvents
iterator: keyCommandIterator
scruffysKeyboard = new KeyboardEvents
for event of scruffysKeyboard
console.log scruffysKeyboard
# UP
# UP
# DOWN
UP = 1
DOWN = -1
keyboardEvents = ->
if Math.floor Math.random()*10 > 5
UP
else
DOWN
const UP = 1;
const DOWN = -1;
function* keyboardEventGenerator () {
for (;;) {
if(Math.floor(Math.random()*10) > 5) {
yield UP;
}
else {
yield DOWN;
}
}
}
keyboardEventGenerator = ->*
while true
if Math.floor Math.random()*10 > 5
yield UP
else
yield DOWN
13.4 Source maps for debugging
Reference Error: x is not defined -- program.js 23
double = (array) ->
throw new Error 'Using source maps'
item * 2 for item in array
double [1,2,3,4]
> coffee sourcemaps.coffee
> coffee --map sourcemaps.coffee
{
"version":3,
"file":" sourcemaps.coffee",
"sources":[
"sourcemaps.coffee"
],
"names":[],
"mappings":"AAAC;;;EAAA,MAAA,GAAS,SAAA,CAAA,KAAA,CAAA;;;MAAW,2BAAqB,aAArB,
aAAA,CAAA,KAAA,CAAA;QAAa,OAAQ;oBAArB,IAAA,CAAA,CAAA,CAAO;;;;;EAC3B,OAAO,IAAP,CAAY,MAAA,CAAO,CAAA;AAAA,IAAC,CAAD;AAAA,IAAG,CAAH;AAAA,IAAK,CAAL;
AAAA,IAAO,CAAP;AAAA,EAAA,CAAP,CAAZ;EACA,KAAA,CAAM,GAAA,CAAI,KAAJ,CAAU,
mBAAV,CAAN"
}
echo '\n//@ sourceMappingURL=sourcemaps.js.map' >> sourcemaps.js
Uncaught Error: Using source maps
double -- sourcemap.js:5
Uncaught Error: Using source maps
double -- sourcemaps.coffee:2
13.5 Summary