Skip to content

Instantly share code, notes, and snippets.

@nornagon
Created August 21, 2013 17:27
Show Gist options
  • Save nornagon/6297367 to your computer and use it in GitHub Desktop.
Save nornagon/6297367 to your computer and use it in GitHub Desktop.
{Collection} = masala = require './masala'
live = require './sugar'
data = new Collection [live {
name: 'list of things'
data: new Collection [live({val:3}),live({val:4})]
}]
http = require 'http'
render = require './render'
page = render data
server = http.createServer (req, res) ->
res.write page.asText()
res.write """
<script>
#{masala.client}
#{live.client}
</script>
<script>
var data = #{JSON.stringify data};
var render = #{render.toString()};
</script>
"""
for s in (render.scripts ? [render.script] ? [])
res.write "<script>(#{s})()</script>"
res.end()
server.listen 3000
console.log 'listening on 3000'
class Element
wrap = (ctx, fn) -> fn()
@track: (fn) -> wrap = fn
constructor: (@tagName, args...) ->
@contentFn = ->
@classList = []
@attributes = {}
@events = {}
@listeners = {}
for a in args
if typeof a is 'function'
@contentFn = a
else if typeof a is 'string'
@spec a
else
@attr a
@rerender()
rerender: ->
@events = {}
# TODO: redo spec/attributes on rerender
@children = []
wrap this, =>
v = @contentFn.call this
if typeof v is 'string' or typeof v is 'number'
@text v
if @sister
newEl = @reify()
@sister.parentNode.replaceChild newEl, @sister
@attach newEl
reify: ->
e = document.createElement @tagName
for k,v of @attributes
e.setAttribute k, v
e.setAttribute 'class', @classList.join(' ') if @classList.length
for c in @children
e.appendChild c.reify()
e
spec: (s) ->
parts = s.split /(?=[.#])/
for p in parts
if p[0] is '#'
@attr id: p.substr 1
else if p[0] is '.'
@classList.push p.substr 1
attr: (obj) ->
for k,v of obj
if k is 'class'
@classList = if v instanceof Array then v else [v]
else
@attributes[k] = v
return @
on: (ev, handler) ->
(@events[ev] ?= []).push handler
emit: (ev, args...) ->
if @listeners[ev]
@listeners[ev] args...
else
@parent?.emit ev, args...
Tags = ['head', 'body', 'ul', 'li', 'div', 'span', 'h1', 'script', 'style', 'link', 'title', 'button', 'textarea']
for tagName in Tags
do (tagName) =>
@::[tagName] = (args...) ->
e = new Element tagName, args...
e.parent = this
@children.push e
e
text: (str) -> @children.push n = new TextNode str; n
each: (collection, f) ->
obs = collection.observe ? (ls) -> ls.insert(k,i) for k,i in @
obs.call collection,
insert: (k, i) =>
# TODO what if the each has >1 child?
e = (new Element 'dummy', -> f.call this, k).children[0]
e.parent = this
@children.splice i, 0, e
if @sister
e_sis = e.reify()
e.attach e_sis
@sister.insertBefore e_sis, @sister.childNodes[i]
remove: (i) =>
@children.splice i, 0
if @sister
@sister.removeChild @sister.childNodes[i]
return @children
json: (name, data) ->
@script -> "#{name} = #{JSON.stringify data}"
part: (partial, args...) ->
partial.apply this, args
attach: (node) ->
#if node.childNodes.length != @children.length
# throw new Error 'mismatched number of nodes'
if node.tagName != @tagName.toUpperCase()
throw new Error "mismatched tag name: #{node.tagName}. Expected #{@tagName.toUpperCase()}."
throw new Error 'mismatched id' unless node.id == (@attributes.id ? "")
@sister = node
for ev,hs of @events
for h in hs
@sister.addEventListener ev, h
nextChild = @sister.firstChild
for c,i in @children
loop
try
c.attach nextChild
break
catch e
console.warn "Skipping unexpected #{nextChild.tagName} element..."
nextChild = nextChild.nextSibling
break if not nextChild
break if not nextChild
nextChild = nextChild.nextSibling
# TODO can throw a weird error here if the doc isn't full yet
node
asText: ->
# TODO escaping
tagExtra = ("#{k}=\"#{v}\"" for k,v of @attributes)
if @classList.length then tagExtra.push 'class="'+@classList.join(' ')+'"'
tagExtra = tagExtra.join(' ')
tagExtra = ' ' + tagExtra if tagExtra.length
"<#{@tagName+tagExtra}>#{(c.asText() for c in @children).join('')}</#{@tagName}>"
class TextNode
constructor: (textContent) -> @textContent = String(textContent)
asText: -> @textContent
reify: -> document.createTextNode @textContent
attach: (node) ->
if node.nodeType != document.TEXT_NODE
throw Error 'mismatched node type'
if node.textContent != @textContent
throw Error 'mismatched text content'
@sister = node
class Fragment extends Element
constructor: (f) ->
super 'dummy', f
attach: (parent) ->
if parent.childNodes.length != @children.length
throw new Error 'mismatched number of nodes'
for c,i in @children
c.attach parent.childNodes[i]
parent
asText: ->
(c.asText() for c in @children).join('')
html = (f) ->
new Element 'html', f
render = (f) ->
new Fragment f
((es) ->
if typeof window is 'undefined'
module.exports = es
if /coffee$/.test __filename
module.exports.client = require('coffee-script').compile require('fs').readFileSync(__filename, 'utf8')
else
module.exports.client = require('fs').readFileSync(__filename, 'utf8')
else
window[k] = v for k,v of es
) {
html
render
Element
}
{html} = require './masala'
module.exports = (data) ->
renderData = (data) ->
@ul '.data', ->
@each data, (i) ->
@li ->
@text i.name
@ul ->
@each i.data, (e) ->
@li -> e.val
hfwrap = (f) ->
html ->
@head ->
@body ->
@h1 -> 'header'
@part f
@div -> 'footer'
hfwrap ->
@div '#main.foo', ->
@part renderData, data
data = undefined
module.exports.script = ->
toLive = (obj) ->
if obj instanceof Array
return new Collection obj.map (e) -> toLive e
if typeof obj is 'object'
for k,v of obj
obj[k] = toLive v
return live obj
obj
window.toLive = toLive
data = toLive data
Element.track live.track (el) -> el.rerender()
page = render data
page.attach document.documentElement
window.page = page
window.data = data
render = (ctx) ->
html ->
@head ->
@link ...
@script ...
@body ->
@div ->
@h1 'hi'
@ul ->
@each ctx.foo, (i) -> @li -> i.name
@script ->
# Properties that aren't on the object at live() call time won't be tracked.
live = (obj) ->
res = Object.create obj
listeners = {}
for k,v of obj
do (k) ->
Object.defineProperty res, k,
get: -> live.context?(iface, k); obj[k]
set: (v) ->
obj[k] = v
if listeners[k]
l(iface, k) for l in listeners[k]
v
Object.defineProperty res, 'toJSON', value: -> obj
Object.defineProperty res, 'unbound', value: obj
# TODO maybe defineProperty non-enumerable on |res| instead?
iface = Object.create res
iface.notify = (key, l) ->
return if listeners[key]?.indexOf(l) >= 0
(listeners[key] ?= []).push l
iface.unnotify = (key, l) ->
return unless key of listeners
listeners[key] = (x for x in listeners[key] when x isnt l)
res
live.with = (ctx, fn) ->
x = live.context
live.context = ctx
fn()
live.context = x
live.track = (onUpdate) -> (el, fn) ->
keys = {}
changed = (obj, k) ->
for k,_ of keys
obj.unnotify k, changed
keys = {}
onUpdate el
got = (obj, k) ->
# obj[k] was read while running |fn|.
obj.notify k, changed
keys[k] = true
live.with got, fn
#Element.track live.track (el) -> el.rerender()
live.array = (a) ->
a = a.map (e) -> live.from e
observers = []
o = Object.create a
o.observe = (obs) ->
obs.insert obj, i for obj, i in a
observers.push obs
o
o.insert = (e, i=@length) -> @splice i, 0, e
o.push = (e) -> @insert e; @length
o.remove = (i) -> @splice i, 0
o.splice = (idx, num, es...) ->
es = (live.from(e) for e in es) # TODO, does this make sense?
r = a.splice idx, num, es...
for [0...num]
o.remove idx for o in observers
for e,i in es
o.insert e, idx+i for o in observers
r
o.toJSON = -> a
o
live.from = (obj) ->
if obj instanceof Array
return live.array obj
if typeof obj is 'object'
new_obj = {}
for k,v of obj
new_obj[k] = live.from v
return live new_obj
obj
if typeof window is 'undefined'
module.exports = live
if /coffee$/.test __filename
module.exports.client = require('coffee-script').compile require('fs').readFileSync(__filename, 'utf8')
else
module.exports.client = require('fs').readFileSync(__filename, 'utf8')
else
window.live = live
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment