author | Alan Dipert
<alan@dipert.org> 2021-05-28 06:20:12 UTC |
committer | Alan Dipert
<alan@dipert.org> 2021-05-28 06:20:12 UTC |
parent | 1e4949c0762a65a3632963df18cd797245ea8065 |
el.mjs | +78 | -16 |
reactives.mjs | +13 | -3 |
todo.mjs | +101 | -57 |
diff --git a/el.mjs b/el.mjs index c6aea31..2089485 100644 --- a/el.mjs +++ b/el.mjs @@ -1,19 +1,44 @@ -import { database, view, watch } from './reactives.mjs'; +import { _ } from './datalog.mjs'; +import { Input, Formula, input, database, view, watch } from './reactives.mjs'; + +const on = new Proxy({}, { + get: (obj, name) => Symbol.for(name) +}); function el(tag, attrs, kids) { let el = document.createElement(tag); - for (const [k, v] of Object.entries(attrs)) { - el.setAttribute(k, v); + for (const k of Reflect.ownKeys(attrs)) { + let v = attrs[k]; + if (v instanceof Input || v instanceof Formula) { + watch((oldv, newv) => { + el.setAttribute(k, newv); + })(v); + } else if (typeof k === "symbol") { + el.addEventListener(k.description, v) + } else { + el.setAttribute(k, v); + } } for (const kid of kids) { - el.appendChild(kid); + if (kid instanceof Input || kid instanceof Formula) { + let textNode = document.createTextNode(""); + watch((oldv, newv) => { + textNode.textContent = newv; + })(kid); + el.appendChild(textNode); + } else { + el.appendChild(kid); + } } return el; } const $el = new Proxy({}, { get: (obj, name) => (attrs = {}, ...kids) => { - if (typeof attrs === "string" || attrs instanceof Node) { + if (typeof attrs === "string" + || attrs instanceof Node + || attrs instanceof Input + || attrs instanceof Formula) { kids.unshift(attrs); attrs = {}; } @@ -23,26 +48,63 @@ const $el = new Proxy({}, { } }); -// renderChildren lifecycle -// renderChildren children lifecycle -// nesting -function renderChildren(parent, eidView, klass) { - let children = new Map(); +function mapEntities(parent, db, eidView, attrs, makeChild) { + let pool = [], + childEids = new Map(); watch((added, removed) => { for (let [eid] of removed) { - parent.removeChild(children.get(eid).el); - children.get(eid).destroy(); - children.delete(eid); + let child = [...childEids].filter(([k, v]) => v.value === eid)[0][0]; + parent.removeChild(child); } for (let [eid] of added) { - children.set(eid, new klass(eid)); - parent.appendChild(children.get(eid).el) + let child; + if (pool.length) { + child = pool.pop(); + childEids.get(child).set(eid); + } else { + let eidCell = input(eid), + attrCells = {eid: eidCell}; + for (let attr of attrs) { + attrCells[attr] = input(null); + watch((added, removed) => { + if (added.size()) { + let [[newv]] = added; + attrCells[attr].set(newv); + } else if (removed.size()) { + attrCells[attr].set(""); + } + })(view({ + find: [_.attrVal], + use: [_.eid], + where: [[_.eid, attr, _.attrVal]] + })(db, eidCell)) + } + child = makeChild(attrCells); + childEids.set(child, eidCell); + } + parent.appendChild(child); } })(eidView); return parent; } -export { $el, renderChildren }; +// function renderChildren(parent, eidView, klass) { +// let children = new Map(); +// watch((added, removed) => { +// for (let [eid] of removed) { +// parent.removeChild(children.get(eid).el); +// children.get(eid).destroy(); +// children.delete(eid); +// } +// for (let [eid] of added) { +// children.set(eid, new klass(eid)); +// parent.appendChild(children.get(eid).el) +// } +// })(eidView); +// return parent; +// } + +export { $el, on, mapEntities }; // function renderInto(parent, view, templateFunction) { // } diff --git a/reactives.mjs b/reactives.mjs index fd503ed..4ed91b5 100644 --- a/reactives.mjs +++ b/reactives.mjs @@ -222,13 +222,23 @@ class Watch { this.callback = callback; this.source = source; addEdge(depGraph, source, this) - this.callback.call(null, this.source.set, new JSONSet()); + if (this.source instanceof Input + || this.source instanceof Formula) { + this.callback.call(null, this.source.previousValue, this.source.value) + } else { + this.callback.call(null, this.source.set, new JSONSet()); + } } detach() { removeEdge(depGraph, this.source, this); } update() { - this.callback.call(null, this.source.added, this.source.removed) + if (this.source instanceof Input + || this.source instanceof Formula) { + this.callback.call(null, this.source.previousValue, this.source.value) + } else { + this.callback.call(null, this.source.added, this.source.removed) + } } flush() { // NOOP @@ -255,7 +265,7 @@ function watch(f) { return (source) => new Watch(f, source); } -export { transaction, input, formula, database, view, watch }; +export { Input, Formula, transaction, input, formula, database, view, watch }; // let A = input(100), // B = input(200), diff --git a/todo.mjs b/todo.mjs index e984bb6..8351bf7 100644 --- a/todo.mjs +++ b/todo.mjs @@ -1,67 +1,111 @@ import { _ } from './datalog.mjs'; -import { transaction, database, view, watch } from './reactives.mjs'; -import { $el, renderChildren } from './el.mjs'; - -let todos = database([ - [0, "todo/text", "do the thing"], - [0, "todo/status", "ready"], - [1, "todo/text", "do the other thing"], - [1, "todo/status", "ready"], - [2, "todo/text", "do that last thing"], - [2, "todo/status", "done"] -]); - -class TodoItem { - constructor(eid) { - this.eid = eid; - this.el = $el.li(document.createTextNode("")); - - watch(([[newText]]) => { - this.el.firstChild.textContent = newText; - })(view({ - find: [_.text], - use: [_.eid], - where: [[_.eid, "todo/text", _.text]] - })(todos, eid)); - - watch(([[newState]]) => { - if (newState === "done") { - this.el.style = "text-decoration: line-through;"; - } else { - this.el.style = ""; - } - })(view({ - find: [_.status], - use: [_.eid], - where: [[_.eid, "todo/status", _.status]] - })(todos, eid)); - - this.el.addEventListener('click', e => { - todos.remove([...todos].filter(([eid]) => eid === this.eid)); - }); - } - // update([id, text]) { - // this.el.removeChild(this.el.firstChild); - // this.el.appendChild(document.createTextNode(text)); - // } - destroy() { - console.log("destroy eid", this.eid); - // TODO - } +import { transaction, input, formula, database, view, watch } from './reactives.mjs'; +import { $el, on, mapEntities } from './el.mjs'; + +function nextEid(db) { + return ([...db].map(([eid]) => eid).sort((x, y) => y - x)[0] || 0) + 1; } function todoList() { - return renderChildren( - $el.ul(), - view({ - find: [_.id], - where: [[_.id, "todo/text", _._]] - })(todos), - TodoItem + let todos = database([ + [0, "text", "do the thing"], + [0, "status", "ready"], + [1, "text", "do the other thing"], + [1, "status", "ready"], + [2, "text", "do that last thing"], + [2, "status", "done"] + ]); + let textInput = $el.input({type: "text"}); + return $el.div( + $el.form({ + [on.submit]: e => { + let newEid = nextEid(todos); + todos.add([ + [nextEid, "text", textInput.value], + [nextEid, "status", "ready"] + ]) + e.preventDefault(); + } + }, + textInput, + $el.input({type: "submit", value: "Add"}), + ), + mapEntities( + $el.ul(), + todos, + view({find: [_.eid], where: [[_.eid, "text", _._]]})(todos), + ["text", "status"], + ({eid, text, status}) => $el.li( + $el.input({ + type: "button", + value: "x", + [on.click]: todos.remove([...todos].filter(([x]) => x === eid.value)) + }), + $el.span({ + style: formula(s => s === "done" ? "text-decoration: line-through" : "")(status), + [on.click]: () => transaction(() => { + todos.remove([[eid.value, "status", status.value]]); + todos.add([[eid.value, "status", status.value === "done" ? "ready" : "done"]]); + }) + }, text) + ) + ) ); } -document.body.appendChild(todoList()) +document.addEventListener("DOMContentLoaded", () => { + document.body.appendChild(todoList()); +}); + +// window.todos = todos; + +// class TodoItem { +// constructor(eid) { +// this.eid = eid; +// this.el = $el.li(document.createTextNode("")); + +// watch(([[newText]]) => { +// this.el.firstChild.textContent = newText; +// })(view({ +// find: [_.text], +// use: [_.eid], +// where: [[_.eid, "todo/text", _.text]] +// })(todos, eid)); + +// watch(([[newState]]) => { +// if (newState === "done") { +// this.el.style = "text-decoration: line-through;"; +// } else { +// this.el.style = ""; +// } +// })(view({ +// find: [_.status], +// use: [_.eid], +// where: [[_.eid, "todo/status", _.status]] +// })(todos, eid)); + +// this.el.addEventListener('click', e => { +// todos.remove([...todos].filter(([eid]) => eid === this.eid)); +// }); +// } +// destroy() { +// console.log("destroy eid", this.eid); +// // TODO +// } +// } + +// function todoList() { +// return renderChildren( +// $el.ul(), +// view({ +// find: [_.id], +// where: [[_.id, "todo/text", _._]] +// })(todos), +// TodoItem +// ); +// } + +// document.body.appendChild(todoList()) // let lastId = [...todos.set] // .map(([id]) => id)