aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2018-05-04 23:06:20 +0000
committers-ol <s-ol@users.noreply.github.com>2018-05-04 23:06:20 +0000
commita26bd7d17cb0782da0c57bb10f6f3ffa4a0f070f (patch)
tree441ced6e7734c618cd078cf63a818a1eb6567f58
parentadd component framework + todo (diff)
downloadmmm-a26bd7d17cb0782da0c57bb10f6f3ffa4a0f070f.tar.gz
mmm-a26bd7d17cb0782da0c57bb10f6f3ffa4a0f070f.zip
add test suite
-rw-r--r--app/centerofmass.moon38
-rw-r--r--app/component.moon73
-rw-r--r--app/html.moon26
-rw-r--r--app/index.moon5
-rw-r--r--app/test_component.moon187
-rw-r--r--app/todo.moon28
m---------dist0
7 files changed, 295 insertions, 62 deletions
diff --git a/app/centerofmass.moon b/app/centerofmass.moon
index d206fc4..1251096 100644
--- a/app/centerofmass.moon
+++ b/app/centerofmass.moon
@@ -76,21 +76,25 @@ app = CenterOfMass "What's up", "Times New Roman", 40
document.body\appendChild app.canvas
app.canvas.style.backgroundColor = '#eee'
-font_input = input type: 'text', value: 'Times New Roman'
-document.body\appendChild div {
- span 'font: ',
- font_input,
- button {
- onclick: (e) => app.font = font_input.value,
- 'set'
+add = =>
+ document.body\appendChild div {
+ span 'font: ',
+ with @font_input = input!
+ .type = 'text'
+ .value = 'Times New Roman'
+ with button 'set'
+ .onclick = (_, e) -> app.font = @font_input.value
}
-}
-
-size_label = span '40'
-document.body\appendChild div {
- span 'font: ',
- input type: 'range', min: 2, max: 120, value: 40, onchange: (e) =>
- size_label.innerText = e.target.value
- app.size = e.target.value
- size_label
-}
+add {}
+
+add = =>
+ document.body\appendChild div {
+ span 'font: ',
+ input type: 'range', min: 2, max: 120, value: 40, onchange: (_, e) ->
+ size = e.target.value
+ @size_label.innerText = size
+ app.size = size
+ with @size_label = span '40'
+ ''
+ }
+add {}
diff --git a/app/component.moon b/app/component.moon
index 1f56679..86458bd 100644
--- a/app/component.moon
+++ b/app/component.moon
@@ -1,20 +1,26 @@
{ :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 'string' == type val
- val = document\createTextNode val
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}"
-iscallback = (val) -> 'table' == (type val) and val.subscribe
+class ReactiveVar
+ @isinstance: (val) -> 'table' == (type val) and val.subscribe
-class Callback
new: (@value) =>
@listeners = setmetatable {}, __mode: 'kv'
@@ -27,35 +33,47 @@ class Callback
get: => @value
subscribe: (callback) =>
- with callback
+ with -> @listeners[callback] = nil
@listeners[callback] = callback
- unsubscribe: (callback) =>
- @listeners[callback] = nil
-
- chain: (transform) =>
- with Callback transform @value
+ map: (transform) =>
+ with ReactiveVar transform @value
.upstream = @subscribe (...) -> \set transform ...
-class CallbackElement
- new: (element, attributes, ...) =>
+class ReactiveElement
+ @isinstance: (val) -> 'table' == (type val) and val.node
+
+ new: (element, ...) =>
@node = document\createElement element
- @_callbacks = {}
+ @_subscriptions = {}
children = { ... }
-
- if 'table' != (type attributes) or attributes.__class == @@ or attributes.__class == Callback
- table.insert children, 1, attributes
+ -- 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 value.__class == Callback
- table.insert @_callbacks, value\subscribe (...) -> @set attr, ...
+ 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
@@ -66,27 +84,32 @@ class CallbackElement
@node[attr] = value
append: (child, last) =>
- if iscallback child
- table.insert @_callbacks, child\subscribe (...) -> @append ...
+ 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\removeChild last
- @node\appendChild child
+ @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 = {
- :Callback,
- :CallbackElement,
+ :ReactiveVar,
+ :ReactiveElement,
:text,
}
- add = (e) -> exports[e] = (...) -> CallbackElement e, ...
+ 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
diff --git a/app/html.moon b/app/html.moon
index 3565f32..fdec714 100644
--- a/app/html.moon
+++ b/app/html.moon
@@ -1,13 +1,25 @@
document = js.global.document
-element = (element) -> (attrs = {}, ...) ->
- if 'table' != type attrs
- attrs = { attrs, ... }
+element = (element) -> (...) ->
+ children = { ... }
+
+ -- attributes are last arguments but mustn't be a ReactiveVar
+ attributes = children[#children]
+ if 'table' == (type attributes) and not attributes.node
+ table.remove children
+ else
+ attributes = {}
+
with e = document\createElement element
- for k,v in pairs attrs
- continue unless 'string' == type k
+ for k,v in pairs attributes
e[k] = v
- for child in *attrs
+
+ -- if there is only one argument,
+ -- children can be in attributes table too
+ if #children == 0
+ children = attributes
+
+ for child in *children
if 'string' == type child
e.innerHTML ..= child
else
@@ -16,7 +28,7 @@ element = (element) -> (attrs = {}, ...) ->
elements = {}
add = (e) -> elements[e] = element e
-for e in *{'div', 'span', 'a', 'p', 'button', 'ul', 'li', 'i', 'b', 'u', 'tt'} do add e
+for e in *{'div', 'span', 'a', 'p', 'pre', 'button', 'ul', 'li', 'i', 'b', 'u', 'tt'} do add e
for e in *{'br', 'img', 'input', 'p'} do add e
for i=1,8 do add "h" .. i
diff --git a/app/index.moon b/app/index.moon
index e43b318..c05edcd 100644
--- a/app/index.moon
+++ b/app/index.moon
@@ -17,6 +17,9 @@ switch window.location.search
when '?todo' then
back_button!
require './todo.moon'
+ when '?test-component' then
+ back_button!
+ require './test_component.moon'
else
document.body\appendChild h1 'mmm'
document.body\appendChild p {
@@ -32,7 +35,7 @@ switch window.location.search
'.'
}
document.body\appendChild p 'current demos:'
- document.body\appendChild ul for name in *{'twisted', 'center-of-mass', 'todo'}
+ document.body\appendChild ul for name in *{'twisted', 'center-of-mass', 'todo', 'test-component'}
li a { name, href: "/?#{name}" }
document.body\appendChild p {
diff --git a/app/test_component.moon b/app/test_component.moon
new file mode 100644
index 0000000..8e2f95f
--- /dev/null
+++ b/app/test_component.moon
@@ -0,0 +1,187 @@
+{ :document, Node } = js.global
+
+import div, h1, ul, li, pre from require './html.moon'
+
+test_group = (name) ->
+ list = ul!
+ root = div (h1 name), list
+ root, (name, test) ->
+ ok, err = pcall test
+ list\appendChild li if ok
+ "passed '#{name}'"
+ else
+ "failed '#{name}'", pre err
+
+node, run_test = test_group 'component.moon'
+document.body\appendChild node
+
+local ReactiveVar, ReactiveElement
+run_test "exports ReactiveVar, ReactiveElement", ->
+ import ReactiveVar, ReactiveElement from require './component.moon'
+ assert ReactiveVar, "ReactiveVar not exported"
+ assert ReactiveElement, "ReactiveElement not exported"
+
+run_test "exports text helper", ->
+ import text from require './component.moon'
+ assert 'function' == (type text), "ReactiveVar not exported"
+
+ node = text 'a test string'
+ assert (js.instanceof node, js.global.Node), "expected text to generate a Node"
+ assert node.data == 'a test string', "expected text to store the string"
+
+run_test "text joins multiple arguments", ->
+ import text from require './component.moon'
+
+ node = text 'a', 'test', 'string'
+ assert node.data == 'a test string', "expected text to join arguments with spaces"
+
+node, run_test = test_group 'ReactiveVar'
+document.body\appendChild node
+
+run_test "stores a value", ->
+ reactive = ReactiveVar 'test'
+ assert 'test' == reactive\get!, "expected x to be 'test'"
+
+run_test "propagates updates", ->
+ local done
+
+ reactive = ReactiveVar 'test'
+ reactive\subscribe coroutine.wrap (next) ->
+ assert next == 'toast', "expected next to be 'toast'"
+ assert coroutine.yield! == 'cheese', "expected next to be 'cheese'"
+ done = true
+
+ reactive\set 'toast'
+ assert 'toast' == reactive\get!, "expected #get to return 'toast'"
+ reactive\set 'cheese'
+ assert done, "expected to reach the end"
+
+run_test "passed old value as well", ->
+ local done
+
+ reactive = ReactiveVar 1
+ reactive\subscribe coroutine.wrap (next, last) ->
+ assert last == 1, "expected last:1 to be 1"
+ next, last = coroutine.yield!
+ assert last == 2, "expected last:2 to be 2"
+ done = true
+
+ reactive\set 2
+ reactive\set 3
+ assert done, "expected to reach the end"
+
+run_test "#subscribe returns function to unsubscribe", ->
+ calls = 0
+
+ reactive = ReactiveVar 1
+ unsub = reactive\subscribe coroutine.wrap () ->
+ calls += 1
+ coroutine.yield!
+ calls += 1
+ coroutine.yield!
+ calls += 1
+
+ assert 'function' == (type unsub), "expected to receive a function"
+
+ reactive\set 2
+ reactive\set 3
+ assert calls == 2, "wat"
+
+ unsub!
+ reactive\set 4
+ assert calls == 2, "expected to stop receiving updates"
+
+run_test "tracks multiple subscriptions at once", ->
+ reactive = ReactiveVar 'test'
+ unsub = reactive\subscribe coroutine.wrap (next) ->
+ assert next == 'toast', "expected next to be toast"
+ next = coroutine.yield!
+ assert next == 'cheese', "expected next to be cheese"
+ coroutine.yield!
+ error "expected not to get here"
+
+ reactive\set 'toast'
+
+ local done
+ reactive\subscribe coroutine.wrap (next) ->
+ assert next == 'cheese', "expected next to be cheese"
+ next = coroutine.yield!
+ assert next == 'test', "expected next to be test"
+ done = true
+
+ reactive\set 'cheese'
+ unsub!
+ reactive\set 'test'
+ assert done, "expected to reach the end"
+
+node, run_test = test_group 'ReactiveElement'
+document.body\appendChild node
+
+run_test "creates a HTML element", ->
+ elem = ReactiveElement 'span'
+ assert elem.node and elem.node.localName == 'span', "expected Node to be a <span>"
+
+run_test "sets attributes from a table arg", ->
+ elem = ReactiveElement 'span', class: 'never'
+ assert elem.node.class == 'never', "expected class to be 'never'"
+
+run_test "appends Nodes from arguments", ->
+ e_div, e_pre = div!, pre!
+ elem = ReactiveElement 'span', e_div, e_pre
+ assert elem.node.firstElementChild == e_div, "expected div to be the first child of elem"
+ assert elem.node.lastElementChild == e_pre, "expected pre to be the last child of elem"
+
+run_test "can append ReactiveElements and text", ->
+ e_div = ReactiveElement 'div'
+ elem = ReactiveElement 'div', e_div, 'testtext'
+ assert elem.node.firstElementChild == e_div.node, "expected div to be the first child of elem"
+ assert elem.node.lastChild.data == 'testtext', "expected last child of elem to be 'testtext'"
+
+run_test "accepts attributes after children", ->
+ e_div = div!
+ elem = ReactiveElement 'div', e_div, class: 'test'
+ assert elem.node.firstElementChild == e_div, "expected div to be the first child of elem"
+ assert elem.node.class == 'test', "expected class to be 'test'"
+
+run_test "allows mixing attributes and children in a single table", ->
+ e_div, e_pre = div!, pre!
+ elem = ReactiveElement 'div', { class: 'test', e_div, e_pre }
+ assert elem.node.firstElementChild == e_div, "expected div to be the first child of elem"
+ assert elem.node.lastElementChild == e_pre, "expected pre to be the last child of elem"
+ assert elem.node.class == 'test', "expected class to be 'test'"
+
+run_test "can unwrap and track attributes from ReactiveVars", ->
+ klass = ReactiveVar 'test'
+ elem = ReactiveElement 'div', class: klass
+ assert elem.node.class == 'test', "expected class to be 'test'"
+ klass\set 'toast'
+ assert elem.node.class == 'toast', "expected class to be 'toast'"
+
+run_test "can unwrap and track children from ReactiveVars", ->
+ child = ReactiveVar h1 'test'
+ elem = ReactiveElement 'div', child, pre 'fixed'
+ assert elem.node.firstElementChild.localName == 'h1', "expected first child to be h1"
+ assert elem.node.childElementCount == 2, "expected node to have two children"
+ child\set div 'toast'
+ assert elem.node.firstElementChild.localName == 'div', "expected first child to be div"
+ assert elem.node.childElementCount == 2, "expected node to have two children"
+
+run_test "warns when appending a string from a ReactiveVar", ->
+ import text from require './component.moon'
+ export print
+ _print = print
+
+ calls = 0
+ print = (msg) -> calls += 1 if 'WARN: string' == msg\sub 1, 12
+
+ str = ReactiveVar 'test'
+ elem = ReactiveElement 'div', str
+ assert calls == 1, "expected to print warning"
+
+ str\set 'string too'
+ assert calls == 2, "expected to print warning again"
+
+ str\set text 'this is text'
+ assert calls == 2, "expected not to print warning with text node"
+
+ print = _print
diff --git a/app/todo.moon b/app/todo.moon
index c38ce26..a464d0d 100644
--- a/app/todo.moon
+++ b/app/todo.moon
@@ -1,30 +1,34 @@
{ :document } = js.global
-import Callback, CallbackElement, text, div, form, span, h3, a, input, textarea, button from require './component.moon'
+import ReactiveVar, text, div, form, span, h3, a, input, textarea, button from require './component.moon'
parent = div!
todoItem = (desc, done) ->
-- convert into reactive data sources
- desc, done = (Callback desc), Callback done
+ desc, done = (ReactiveVar desc), ReactiveVar done
with me = div style:
margin: '8px'
padding: '8px'
background: '#eeeeee'
- \append h3 style: 'margin: 0;', desc
- \append span done\chain (done) -> text if done then 'done' else 'not done yet'
+ \append h3 (desc\map text), style: 'margin: 0;'
+ \append span done\map (done) -> text if done then 'done' else 'not done yet'
\append input type: 'checkbox', checked: done, onchange: (e) => done\set e.target.checked
- \append a href: '#', onclick: ((e) => parent\remove me), text 'delete'
+ \append a (text 'delete'), href: '#', onclick: (e) => parent\remove me
parent\append todoItem 'write a Component System'
parent\append todoItem 'eat Lasagna', true
-form = with form action: '', style: 'margin 2px;'
- desc = input type: 'text', value: 'start'
- \append desc
+desc = ReactiveVar 'start'
+form = with form {
+ action: ''
+ style:
+ margin: '2px'
+ onsubmit: (e) =>
+ e\preventDefault!
+ parent\append todoItem desc\get!
+ desc\set ''
+ }
+ \append input type: 'text', value: desc, onchange: (e) => desc\set e.target.value
\append input type: 'submit', value: 'add'
- \set 'onsubmit', (e) =>
- e\preventDefault!
- parent\append todoItem desc.node.value
- desc.node.value = ''
document.body\appendChild parent.node
document.body\appendChild form.node
diff --git a/dist b/dist
-Subproject b1ee5409d7fcf9ee3c24afbcfcce1ef1e20eb7d
+Subproject 114f08fbeb076f247d7e9b960444171f2f6d961