1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
{ :document } = js.global
-- convert anything to a DOM Node
-- val must be one of:
-- * DOM Node (instanceof window.Node)
-- * MMMElement (have a .node value that is instanceof window.Node)
-- * string
-- note that strings won't survive identity comparisons after asnode
asnode = (val) ->
if 'string' == type val
return document\createTextNode val
if 'table' == type val
assert val.node, "Table doesn't have .node"
val = val.node
if 'userdata' == type val
assert (js.instanceof val, js.global.Node), "userdata is not a Node"
val
else
error "not a Node: #{val}, #{type val}"
class ReactiveVar
@isinstance: (val) -> 'table' == (type val) and val.subscribe
new: (@value) =>
@listeners = setmetatable {}, __mode: 'kv'
set: (value) =>
old = @value
@value = value
for k, callback in pairs @listeners
callback @value, old
get: => @value
subscribe: (callback) =>
with -> @listeners[callback] = nil
@listeners[callback] = callback
map: (transform) =>
with ReactiveVar transform @value
.upstream = @subscribe (...) -> \set transform ...
class ReactiveElement
@isinstance: (val) -> 'table' == (type val) and val.node
new: (element, ...) =>
@node = document\createElement element
@_subscriptions = {}
children = { ... }
-- attributes are last arguments but mustn't be a ReactiveVar
attributes = children[#children]
if 'table' == (type attributes) and
(not ReactiveElement.isinstance attributes) and
(not ReactiveVar.isinstance attributes)
table.remove children
else
attributes = {}
for k,v in pairs attributes
@set k, v if 'string' == type k
-- if there is only one argument,
-- children can be in attributes table too
if #children == 0
children = attributes
for child in *children
@append child
destroy: =>
for unsub in *@_subscriptions do unsub!
set: (attr, value) =>
if 'table' == (type value) and ReactiveVar.isinstance value
table.insert @_subscriptions, value\subscribe (...) -> @set attr, ...
value = value\get!
if attr == 'style' and 'table' == type value
for k,v in pairs value
@node.style[k] = v
return
@node[attr] = value
append: (child, last) =>
if ReactiveVar.isinstance child
table.insert @_subscriptions, child\subscribe (...) -> @append ...
child = child\get!
if 'string' == type child
print 'WARN: string from ReactiveVar implicitly converted to TextNode, updating may fail'
child = asnode child
ok, last = pcall asnode, last
if ok
@node\replaceChild child, last
else
@node\appendChild child
remove: (child) =>
@node\removeChild asnode child
if 'table' == (type child) and child.destroy
child\destroy!
text = (...) -> document\createTextNode table.concat { ... }, ' '
with exports = {
:ReactiveVar,
:ReactiveElement,
:text,
}
add = (e) -> exports[e] = (...) -> ReactiveElement e, ...
for e in *{'div', 'form', 'span', 'a', 'p', 'button', 'ul', 'li', 'i', 'b', 'u', 'tt'} do add e
for e in *{'br', 'img', 'input', 'p', 'textarea'} do add e
for i=1,8 do add "h" .. i
|