diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2018-05-04 23:06:20 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2018-05-04 23:06:20 +0000 |
| commit | a26bd7d17cb0782da0c57bb10f6f3ffa4a0f070f (patch) | |
| tree | 441ced6e7734c618cd078cf63a818a1eb6567f58 | |
| parent | add component framework + todo (diff) | |
| download | mmm-a26bd7d17cb0782da0c57bb10f6f3ffa4a0f070f.tar.gz mmm-a26bd7d17cb0782da0c57bb10f6f3ffa4a0f070f.zip | |
add test suite
| -rw-r--r-- | app/centerofmass.moon | 38 | ||||
| -rw-r--r-- | app/component.moon | 73 | ||||
| -rw-r--r-- | app/html.moon | 26 | ||||
| -rw-r--r-- | app/index.moon | 5 | ||||
| -rw-r--r-- | app/test_component.moon | 187 | ||||
| -rw-r--r-- | app/todo.moon | 28 | ||||
| m--------- | dist | 0 |
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 |
