aboutsummaryrefslogtreecommitdiffstats
path: root/lib/component.client.moon
blob: ee0f9ca0d680f688f6040eb6abcd56703f62b424 (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
-- 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 ...

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 last
      error 'cannot replace string node'

    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!

with exports = {
    :ReactiveVar,
    :ReactiveElement,
    :tohtml,
    :append,
    :text,
  }
  add = (e) -> exports[e] = (...) -> ReactiveElement e, ...

  for e in *{'div', 'form', 'span', 'a', 'p', 'button', 'ul', 'ol', 'li', 'i', 'b', 'u', 'tt'} do add e
  for e in *{'article', 'section', 'header', 'footer', 'content'} do add e
  for e in *{'br', 'hr', 'img', 'input', 'p', 'textarea'} do add e
  for i=1,8 do add "h" .. i