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