aboutsummaryrefslogtreecommitdiffstats
path: root/lib/component.server.moon
blob: 0026d326e713cdf871e9bba116f71f15f212b06a (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
import opairs from require 'lib.ordered'

void_tags = { 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' }
void_tags = { t,t for t in *void_tags }

-- convert anything to HTML string
-- val must be one of:
-- * MMMElement (have a .render method that returns a string)
-- * string
tohtml = (val) ->
  if 'table' == type val
    assert val.render, "Table doesn't have .render"
    val = val\render!
  if 'string' == type val
    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 = (...) -> table.concat { ... }, ' '

class ReactiveVar
  @isinstance: (val) -> 'table' == (type val) and val.subscribe

  new: (@value) =>

  set: (value) =>
    error "attempting to update ReactiveVar serverside"

  get: => @value

  subscribe: (callback) =>
    warn "attempting to subscribe to ReactiveVar serverside"

  map: (transform) =>
    ReactiveVar transform @value

class ReactiveElement
  @isinstance: (val) -> 'table' == (type val) and val.render

  new: (element, ...) =>
    @element = element
    @attrs = { style: {} }
    @children = {}

    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: =>

  set: (attr, value) =>
    if attr == 'style' and 'table' == type value
      for k,v in opairs value
        @attrs.style[k] = v
      return

    @attrs[attr] = value

  append: (child, last) =>
    assert not last, "last passed to append on server"
    if ReactiveVar.isinstance child
      child = child\get!

    child = tohtml child
    table.insert @children, child

  remove: (child) =>
    error "remove called serverside"

  render: =>
    b = "<#{@element}"
    for k,v in opairs @attrs
      if 'table' == type v
        tmp = ''
        for kk, vv in opairs v
          tmp ..= "#{kk}: #{vv}; "
        v = tmp
      b ..= " #{k}=\"#{v}\""

    if void_tags[@element]
      assert #@children == 0, "void tag #{element} cannot have children!"
      b .. ">"
    else
      b .. ">"
      b ..= ">" ..  table.concat @children, ''
      b ..= "</#{@element}>"
      b

elements = setmetatable {}, __index: (name) =>
  with val = (...) -> ReactiveElement name, ...
    @[name] = val

get_or_create = (elem, id, ...) ->
  with ReactiveElement elem, ...
    \set 'id', id

{
  :ReactiveVar,
  :ReactiveElement,
  :get_or_create,
  :tohtml,
  :flush,
  :append,
  :text,
  :elements,
}