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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
-- 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 tohtml
tohtml = (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}"
-- overloaded append
-- see tohtml for acceptable values
g_append = append
append = (value) -> g_append tohtml value
-- shorthand to form a text node from strings
text = (...) -> document\createTextNode table.concat { ... }, ' '
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
transform: (transform) => @set transform @get!
subscribe: (callback) =>
with -> @listeners[callback] = nil
@listeners[callback] = callback
map: (transform) =>
with ReactiveVar transform @value
.upstream = @subscribe (...) -> \set transform ...
-- join = do
-- update = (k, new) -> (old) ->
-- with copy = { k, v for k,v in pairs old }
-- copy[k] = new
--
-- (inputs) ->
-- values = {}
-- with ReactiveVar values
-- for k, input in pairs inputs
-- input = inputs[k]
-- values[i] = input\get!
-- input\subscribe (new) -> \transform update k, new
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
print "setting attr #{attr}"
@node[attr] = value
append: (child, last) =>
if ReactiveVar.isinstance child
table.insert @_subscriptions, child\subscribe (...) -> @append ...
child = child\get!
if 'string' == type last
error 'cannot replace string node'
if child == nil
if last
@remove last
return
child = tohtml child
ok, last = pcall tohtml, last
if ok
@node\replaceChild child, last
else
@node\appendChild child
remove: (child) =>
@node\removeChild tohtml child
if 'table' == (type child) and child.destroy
child\destroy!
elements = setmetatable {}, __index: (name) =>
with val = (...) -> ReactiveElement name, ...
@[name] = val
{
:ReactiveVar,
:ReactiveElement,
-- :join,
:tohtml,
:append,
:text,
:elements,
}
|