aboutsummaryrefslogtreecommitdiffstats
path: root/lib/component.client.moon
blob: af0b38a8455424bb336969035657e1eb2f142287 (plain)
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,
}