Created
August 21, 2013 17:27
-
-
Save nornagon/6297367 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{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' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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="'[email protected](' ')+'"' | |
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{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 -> | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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