diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2018-11-10 09:14:44 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2018-11-10 09:14:44 +0000 |
| commit | 5efdd65677f1cb94d7f617accbb0bb798e50eddc (patch) | |
| tree | e62445326374937c0f9b2562c5f7f68acc35213c /root/meta/mmm.component | |
| parent | cleanup a bit (diff) | |
| download | mmm-5efdd65677f1cb94d7f617accbb0bb798e50eddc.tar.gz mmm-5efdd65677f1cb94d7f617accbb0bb798e50eddc.zip | |
begin documenting mmm.component
Diffstat (limited to 'root/meta/mmm.component')
5 files changed, 451 insertions, 0 deletions
diff --git a/root/meta/mmm.component/description: text$plain b/root/meta/mmm.component/description: text$plain new file mode 100644 index 0000000..e55fcd6 --- /dev/null +++ b/root/meta/mmm.component/description: text$plain @@ -0,0 +1 @@ +a small and DOM-centric framework for reactive web interfaces diff --git a/root/meta/mmm.component/tests: text$moonscript -> mmm$dom.moon b/root/meta/mmm.component/tests: text$moonscript -> mmm$dom.moon new file mode 100644 index 0000000..547b030 --- /dev/null +++ b/root/meta/mmm.component/tests: text$moonscript -> mmm$dom.moon @@ -0,0 +1,246 @@ +import article, div, h1, ul, li, pre from require 'mmm.dom' + +_content = {} +append = (stuff) -> table.insert _content, stuff + +last = nil +test_group = (name) -> + if last + append div (h1 last.name), ul last + + last = { :name } + (name, test) -> + ok, err = pcall test + table.insert last, li if ok + "passed '#{name}'" + else + "failed '#{name}'", pre err + +expect = (expected, note, ...) -> + ok, msg = pcall ... + if ok or not msg\find expected + error note + +run_test = test_group 'component.moon' + +local ReactiveVar, ReactiveElement +run_test "exports ReactiveVar, ReactiveElement", -> + import ReactiveVar, ReactiveElement from require 'mmm.component' + assert ReactiveVar, "ReactiveVar not exported" + assert ReactiveElement, "ReactiveElement not exported" + +run_test "exports tohtml helper", -> + import tohtml from require 'mmm.component' + assert 'function' == (type tohtml), "tohtml not exported" + +-- @TODO: get_or_create + +run_test "exports text helper", -> + import text from require 'mmm.component' + assert 'function' == (type text), "text not exported" + + node = text 'a test string' + + data = if MODE == 'CLIENT' + assert (js.instanceof node, js.global.Node), "expected text to generate a Node" + node.data + else + node + + assert data == 'a test string', "expected text to store the string" + +run_test "text joins multiple arguments", -> + import text from require 'mmm.component' + + node = text 'a', 'test', 'string' + + data = if MODE == 'CLIENT' then node.data else node + assert data == 'a test string', "expected text to join arguments with spaces" + +run_test "exports elements table", -> + import elements from require 'mmm.component' + + assert (type elements.div!) == 'table', "expected to build element with elements.div!" + assert (type elements.madeup!) == 'table', "expected to build element with elements.madeup!" + +run_test = test_group 'ReactiveVar' + +run_test "stores a value", -> + reactive = ReactiveVar 'test' + assert 'test' == reactive\get!, "expected x to be 'test'" + +run_test "provides #map shorthand", -> + local done + + original = ReactiveVar 1 + mapped = original\map (a) -> a + 10 + + assert mapped\get! == 11, "expected mapped to be 11" + return if MODE == 'SERVER' + + original\set 4 + assert mapped\get! == 14, "expected mapped to update" + + mapped\subscribe coroutine.wrap (next, last) -> + assert next == 26, "expected next to be 26" + assert last == 14, "expected last to be 14" + done = true + + original\set 16 + assert done, "expected to reach the end" + +if MODE == 'CLIENT' + 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 "passes 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 "provides #transform shorthand", -> + 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 + + add_one = (a) -> a + 1 + reactive\transform add_one + reactive\transform add_one + 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" + +run_test = test_group 'ReactiveElement' + +run_test "creates a HTML element", -> + elem = ReactiveElement 'span' + assert elem.node and elem.node.localName == 'span', "expected Node to be a <span>" + +-- @TODO: can take over a DOM Node + +run_test "sets attributes from a table arg", -> + elem = ReactiveElement 'span', class: 'never' + assert elem.node.className == '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.className == '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.className == '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.className == 'test', "expected class to be 'test'" + klass\set 'toast' + assert elem.node.className == '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 'mmm.component' + + str = ReactiveVar 'test' + elem = ReactiveElement 'div', str + expect 'cannot replace string node', 'expected error', str\set, 'string too' + elem\destroy! + + elem = ReactiveElement 'div', str\map text + str\set 'this is text' + +test_group! + +article _content diff --git a/root/meta/mmm.component/text$moonscript -> fn -> mmm$dom.moon b/root/meta/mmm.component/text$moonscript -> fn -> mmm$dom.moon new file mode 100644 index 0000000..d5acc1e --- /dev/null +++ b/root/meta/mmm.component/text$moonscript -> fn -> mmm$dom.moon @@ -0,0 +1,169 @@ +import article, section, h1, h2, h3, p, a, div, ul, li, pre, code from require 'mmm.dom' +import lua, moonscript from (require 'mmm.highlighting').languages + +mmmcomp = -> code 'mmm.component' + +source = do + (moon_src, lua_src, demo=true) -> + the_code = pre (moonscript moon_src), (lua lua_src), class: 'dual-code' + + return the_code unless demo + + example = assert load lua_src + div the_code, div example!, class: 'example' + +=> article { + h1 mmmcomp! + p mmmcomp!, " is a small and DOM-centric framework for reactive web interfaces." + + p do + fengari = a "fengari.io", href: '//fengari.io' + + "Built for reactive UI, ", mmmcomp!, " is meant to run on the client using ", fengari, ". + However, like ", (code 'mmm.dom'), ", the API is supported both on the client as well as + on the server (were most reactive features have been omitted) so that static pre-rendered + content can still be generated from the same code that powers the reactive interface." + + h2 "Examples" + p "Feel free to read the API documentation below, or take a look at the following examples using the ", + (code 'mmmfs'), " inspect mode:" + + ul for child in *@children + li a (child\gett 'name: alpha'), { + href: child.path, + onclick: (e) => + e\preventDefault! + BROWSER\navigate child.path + } + + h2 "API" + p "Begin by requiring ", mmmcomp!, ". The module returns a table containing the following:" + + ul { + li (code 'ReactiveVar'), ": class/constructor for reactive state variables.", + li (code 'ReactiveElement'), ": class/constructor for reactive DOM elements. Rarely used directly." + li (code 'elements'), ": 'magic table' containing constructors for ReactiveElements by tag name." + li (code 'tohtml'), ": helper to convert from ReactiveElements to mmm/dom (DOM nodes / HTML strings)" + li (code 'text'), ": helper to convert Lua strings to DOM Text Nodes." + li (code 'get_or_create'), ": helper for rehydratable views." + } + + section do + rvar = -> code 'ReactiveVar' + + { + id: 'ReactiveVar' + + h3 "Reactive Variables" + p mmmcomp!, " is centered around the concept of Reactive Variables (", rvar!, "s). + A ", rvar!, " is a container for a piece of application state that other pieces of code can + subscribe to. These attached callbacks are invoked whenever the value changes." + + p "You can instantiate a ", rvar!, " via the constructor at any time. The constructor takes + the initial variable as an argument, but if you omit it ", (code 'nil'), " will work fine as well. + After instantiation, ", (code ':get()'), " and ", (code ':set(val)'), " will give access to the value:" + + source [[ +import ReactiveVar from require 'mmm.component' + +test = ReactiveVar 3 +print test\get! -- prints '3' +test\set 4 +print test\get! -- prints '4' + ]], [[ +local ReactiveVar = require 'mmm.component'.ReactiveVar + +local test = ReactiveVar(3) +print(test:get()) -- prints '3' +test:set(4) +print(test:get()) -- prints '4' + ]], false + + p "The value can also be changed using ", (code ':transform(fn)'), ", which is simply a shorthand for ", + (code 'var:set(fn(var:get()))'), ":" + + source [[ +import ReactiveVar from require 'mmm.component' + +add_one = (n) -> n + 1 +count = ReactiveVar 1 + +count\transform add_one +print test\get! -- prints '2' + +count\transform add_one +print test\get! -- prints '3' + ]], [[ +local ReactiveVar = require 'mmm.component'.ReactiveVar + +local function add_one(x) return x + 1 end +local count = ReactiveVar(1) + +count:transform(add_one) +print(test:get()) -- prints '2' + +count:transform(add_one) +print(test:get()) -- prints '3' + ]], false + + p "Now, so far we haven't really seen anything useful - this is all just behaving like a normal variable. + The ", (code ':subscribe(callback)'), " method is what makes ", rvar!, "s interesting: Whenever the value + changes, the ", rvar!, " calls each of the registered handlers, passing the new as well as the previous value:" + + source [[ +import ReactiveVar from require 'mmm.component' + +add_one = (n) -> n + 1 +count = ReactiveVar 1 +count\subscribe (new, old) -> + print "changing from #{old} to #{new}!" + + +count\transform add_one -- changing from 1 to 2 +count\set "a string" -- changing from 2 to a string + ]], [[ +local ReactiveVar = require 'mmm.component'.ReactiveVar + +local function add_one(x) return x + 1 end +local count = ReactiveVar(1) +cout:subscribe(function(new old) + print("changing from " .. old .. " to " .. new) +end) + +count:transform(add_one) -- changing from 1 to 2 +count:set("a string") -- changing from 2 to a string + ]], false + + p "This allows other code (such as ", (code 'ReactiveElement'), "s) to react to value changes and update + themselves, as we will see in a minute. Often you will want to derive state from other state. To make this + easy while keeping everything reactive, ", mmmcomp!, " includes the ", (code ':map(fn)'), " method." + + p (code ':map(fn)'), " applies the function ", (code 'fn'), " to the current value, just as ", + (code ':transform(fn)'), " would, but it doesn't update the value itself - it rather returns a new ", rvar!, + " instance that is already set up to update whenever the original one changes." + + source [[ +import ReactiveVar from require 'mmm.component' + +fruit = ReactiveVar "apple" +loud_fruit = fruit\map string.upper + +print fruit\get! -- prints 'apple' +print loud_fruit\get! -- prints 'APPLE' + +fruit\set "orange" +print loud_fruit\get! -- prints 'ORANGE' + ]], [[ +local ReactiveVar = require 'mmm.component'.ReactiveVar + +local fruit = ReactiveVar("apple") +local loud_fruit = fruit:map(string.upper) + +print(fruit:get()) -- prints 'apple' +print(loud_fruit:get()) -- prints 'APPLE' + +fruit:set("orange") +print(loud_fruit:get()) -- prints 'ORANGE' + ]], false + } +} diff --git a/root/meta/mmm.component/todoMVC/description: text$plain b/root/meta/mmm.component/todoMVC/description: text$plain new file mode 100644 index 0000000..baaf6fc --- /dev/null +++ b/root/meta/mmm.component/todoMVC/description: text$plain @@ -0,0 +1 @@ +TodoMVC using mmm.component diff --git a/root/meta/mmm.component/todoMVC/text$moonscript -> mmm$component.moon b/root/meta/mmm.component/todoMVC/text$moonscript -> mmm$component.moon new file mode 100644 index 0000000..dc4e5e1 --- /dev/null +++ b/root/meta/mmm.component/todoMVC/text$moonscript -> mmm$component.moon @@ -0,0 +1,34 @@ +import ReactiveVar, text, elements from require 'mmm.component' +import article, div, form, span, h3, a, input from elements + +parent = div! +todoItem = (desc, done) -> + -- convert into reactive data sources + desc, done = (ReactiveVar desc), ReactiveVar done + with me = div style: + margin: '8px' + padding: '8px' + background: '#eeeeee' + \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 (text 'delete'), href: '#', onclick: (e) => parent\remove me + +parent\append todoItem 'write a Component System', true +parent\append todoItem 'eat Lasagna', true +parent\append todoItem 'do other things' + +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' + +article parent, form |
