author | Alan Dipert
<alan@dipert.org> 2021-04-21 06:22:14 UTC |
committer | Alan Dipert
<alan@dipert.org> 2021-04-21 06:22:14 UTC |
parent | 880ed9787e94e77bff80e4035571c01631e6065f |
datalog.mjs | +4 | -1 |
el.js | +0 | -13 |
el.mjs | +61 | -0 |
index.html | +1 | -0 |
reactives.js => reactives.mjs | +4 | -0 |
tests.mjs | +41 | -5 |
todo.html | +10 | -0 |
todo.js | +0 | -150 |
todo.mjs | +173 | -0 |
diff --git a/datalog.mjs b/datalog.mjs index 0568d0b..7e77a70 100644 --- a/datalog.mjs +++ b/datalog.mjs @@ -64,6 +64,9 @@ class JSONSet { size() { return Object.keys(this.values).length; } + toArray() { + return [...this]; + } } class Relation { @@ -200,7 +203,7 @@ function partialQuery(q) { return (...vals) => query(q, ...vals); } -export { _, query }; +export { _, query, JSONSet, partialQuery }; // let db = new JSONSet([ // ["sally", "age", 21], // ["fred", "age", 42], diff --git a/el.js b/el.js deleted file mode 100644 index 8f6df05..0000000 --- a/el.js +++ /dev/null @@ -1,13 +0,0 @@ -function renderInto(parent, view, templateFunction) { -} - -renderInto($el.ol, view({ - find: [_.id], - where: [[_.id _._ _._]] -}, todos, ([idCell]) => $.li( - {class: "todo"}, - view({ - find: [_.text], - use: [_.id], - }) -))) diff --git a/el.mjs b/el.mjs new file mode 100644 index 0000000..3e924dc --- /dev/null +++ b/el.mjs @@ -0,0 +1,61 @@ +import { database, view, watch } from './reactives.mjs'; + +function el(tag, attrs, kids) { + let el = document.createElement(tag); + for (const [k, v] of Object.entries(attrs)) { + el.setAttribute(k, v); + } + for (const kid of kids) { + el.appendChild(kid); + } + return el; +} + +const $el = new Proxy({}, { + get: (obj, name) => (attrs = {}, ...kids) => { + if (typeof attrs === "string" || attrs instanceof Node) { + kids.unshift(attrs); + attrs = {}; + } + return el(name, attrs, kids.map(kid => { + return typeof kid === "string" ? document.createTextNode(kid) : kid; + })); + } +}); + +function renderChildren(parent, view, constructor) { + let children = new Map(); + watch((added, removed) => { + for (let [key] of removed) { + if (children.has(key)) { + parent.removeChild(children.get(key).element); + children.delete(key); + } + } + for (let tuple of added) { + let [key] = tuple; + if (!children.has(key)) { + children.set(key, constructor(tuple)); + parent.appendChild(children.get(key).element) + } else { + children.get(key).update(children.get(key).element, tuple); + } + } + })(view); + return parent; +} + +export { $el, renderChildren }; +// function renderInto(parent, view, templateFunction) { +// } + +// renderInto($el.ol, view({ +// find: [_.id], +// where: [[_.id _._ _._]] +// }, todos, ([idCell]) => $.li( +// {class: "todo"}, +// view({ +// find: [_.text], +// use: [_.id], +// }) +// ))) diff --git a/index.html b/index.html index 05f1c45..0f6f22f 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,7 @@ <script src="qunit-2.9.2.js"></script> <script type="module" src="datalog.mjs"></script> <script type="module" src="reactives.mjs"></script> + <script type="module" src="el.mjs"></script> <script type="module" src="tests.mjs"></script> <!-- <script src="relcells.js"></script> --> <!-- <script src="el.js"></script> --> diff --git a/reactives.js b/reactives.mjs similarity index 98% rename from reactives.js rename to reactives.mjs index 7e1d3ef..fd503ed 100644 --- a/reactives.js +++ b/reactives.mjs @@ -1,3 +1,5 @@ +import { JSONSet, partialQuery } from './datalog.mjs'; + const depGraph = new Map(), suspended = new Set(); let inTransaction = false; @@ -253,6 +255,8 @@ function watch(f) { return (source) => new Watch(f, source); } +export { transaction, input, formula, database, view, watch }; + // let A = input(100), // B = input(200), // C = formula(console.log)(A, B); diff --git a/tests.mjs b/tests.mjs index a3b15ac..4d1dd09 100644 --- a/tests.mjs +++ b/tests.mjs @@ -1,10 +1,46 @@ -import { _, query } from './datalog.mjs'; +import { _, query, JSONSet } from './datalog.mjs'; const { module, test } = QUnit; -const add = (a, b) => a + b; +QUnit.assert.queryEquals = function(expect, q, ...args) { +}; -module('add', () => { - test('should add two numbers', assert => { - assert.equal(add(1, 1), 2, '1 + 1 = 2'); +module('datalog', () => { + + let db1 = new JSONSet([ + ["sally", "age", 21], + ["fred", "age", 42], + ["ethel", "age", 42], + ["fred", "likes", "pizza"], + ["sally", "likes", "opera"], + ["ethel", "likes", "sushi"] + ]); + + let db2 = new JSONSet([ + ["ethel", "sex", "female"], + ["fred", "sex", "male"] + ]); + + test('simple query', assert => { + assert.deepEqual( + query({ + find: [_.e], + use: [[_.db]], + where: [ + [_.db, _.e, "age", 42] + ] + }, db1).toArray(), + [["fred"], ["ethel"]] + ); }); + test('implicit database', assert => { + assert.deepEqual( + query({ + find: [_.e], + where: [ + [_.e, "age", 42] + ] + }, db1).toArray(), + [["fred"], ["ethel"]] + ); + }) }); diff --git a/todo.html b/todo.html new file mode 100644 index 0000000..9a1e14b --- /dev/null +++ b/todo.html @@ -0,0 +1,10 @@ +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width"> + <title>hoplite todo</title> + <script type="module" src="todo.mjs"></script> + </head> + <body> + </body> +</html> diff --git a/todo.js b/todo.js deleted file mode 100644 index 609fc7e..0000000 --- a/todo.js +++ /dev/null @@ -1,150 +0,0 @@ -let todos = new 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"] -]); - -let lastId = [...todos.set] - .map(([id]) => id) - .sort((a, b) => b - a) - .shift(); - -let transitions = new database([ - ["ready", "done"], - ["done", "ready"] -]); - -function todoRemove(id) { - todos.remove([...todos.set].filter(([eid]) => eid === id)); -} - -function todoAdd(text) { - let id = ++lastId; - todos.add([ - [id, "todo/text", text], - [id, "todo/status", "ready"] - ]); -} - -function todoToggle(id) { - let [[from, to]] = query({ - find: [_.from, _.to], - use: [[_.db1, _.db2], _.id], - where: [ - [_.db1, _.id, "todo/status", _.from], - [_.db2, _.from, _.to] - ] - }, todos.set, transitions.set, id); - transaction(() => { - todos.remove([[id, "todo/status", from]]); - todos.add([[id, "todo/status", to]]); - }); -} - -let todoIds = view({ - find: [_.id], - where: [[_.id, _._, _._]] -})(todos); - -let subs = new WeakMap(); - -function makeTodoLi(ul, id) { - let txt = document.createTextNode(""), - li = document.createElement("li"), - deleteButton = document.createElement("button"), - viewer = view({ - find: [_.status, _.text], - use: [_.id], - where: [ - [_.id, "todo/status", _.status], - [_.id, "todo/text", _.text] - ] - })(todos, id), - updater = watch((added) => { - if (added.size()) { - let [[status, text]] = added; - txt.textContent = text; - } - })(viewer); - li.appendChild(txt); - li.dataset.todoId = id; - deleteButton.innerText = "delete"; - li.appendChild(deleteButton); - ul.appendChild(li); - subs.set(li, [viewer, updater]); -} - -function destroyTodoLi(ul, id) { - let li = ul.querySelector(`li[data-todo-id='${id}']`) - subs.get(li).forEach(x => x.detach()); - ul.removeChild(li); -} - -document.addEventListener("DOMContentLoaded", () => { - let ul = document.getElementById("todos-ul"); - watch((added, removed) => { - for (let [id] of added) makeTodoLi(ul, id); - for (let [id] of removed) destroyTodoLi(ul, id); - })(todoIds); -}); -// function findTodoLi(ul, id) { -// for (let li of ul.children) { -// if (li.dataset.id == id) { -// return li; -// } -// } -// } - -// let showStatus = input(ANY.VALUE); -// // let showStatus = input("ready"); - -// let visibleTodos = view({ -// find: [_.id, _.status, _.text], -// use: [_.status], -// where: [ -// [_.id, "todo/text", _.text], -// [_.id, "todo/status", _.status] -// ] -// })(todos, showStatus); - -// let todoTexts = view({ -// find: [_.id, _.text], -// where: [[_.id, _._, _.text]] -// })(visibleTodos); - -// let todoStatuses = view({ -// find: [_.id, _.status], -// where: [[_.id, _.status, _._]] -// })(visibleTodos); - -// document.addEventListener("DOMContentLoaded", () => { -// let ul = document.getElementById("todos-ul"), -// input = document.getElementById("todos-new"), -// add = document.getElementById("todos-add"); -// add.addEventListener("click", () => { -// addTodo(input.value); -// input.value = ""; -// }); -// watch((added, removed) => { -// for (let [id, text] of added) { -// let li = findTodoLi(ul, id); -// if (!li) { -// li = document.createElement("li"); -// li.appendChild(document.createTextNode(text)); -// li.dataset.id = id; -// li.addEventListener("click", () => removeTodo(id)); -// ul.appendChild(li); -// } -// } -// for (let [id] of removed) { -// for (let child of ul.children) { -// if (child.dataset.id == id) { -// ul.removeChild(child); -// } -// } -// } -// })(todoTexts); -// }); diff --git a/todo.mjs b/todo.mjs new file mode 100644 index 0000000..caa72b0 --- /dev/null +++ b/todo.mjs @@ -0,0 +1,173 @@ +import { _ } from './datalog.mjs'; +import { database, view } 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"] +]); + +function todoList() { + return renderChildren( + $el.ul(), + view({ + find: [_.id, _.text], + where: [[_.id, "todo/text", _.text]] + })(todos), + ([id, text]) => ({ + element: $el.li(text), + update: (el, [id, text]) => { + el.removeChild(el.firstChild); + el.appendChild(document.createTextNode(text)); + } + }) + ); +} + +document.body.appendChild(todoList()) + +// let lastId = [...todos.set] +// .map(([id]) => id) +// .sort((a, b) => b - a) +// .shift(); + +// let transitions = new database([ +// ["ready", "done"], +// ["done", "ready"] +// ]); + +// function todoRemove(id) { +// todos.remove([...todos.set].filter(([eid]) => eid === id)); +// } + +// function todoAdd(text) { +// let id = ++lastId; +// todos.add([ +// [id, "todo/text", text], +// [id, "todo/status", "ready"] +// ]); +// } + +// function todoToggle(id) { +// let [[from, to]] = query({ +// find: [_.from, _.to], +// use: [[_.db1, _.db2], _.id], +// where: [ +// [_.db1, _.id, "todo/status", _.from], +// [_.db2, _.from, _.to] +// ] +// }, todos.set, transitions.set, id); +// transaction(() => { +// todos.remove([[id, "todo/status", from]]); +// todos.add([[id, "todo/status", to]]); +// }); +// } + +// let todoIds = view({ +// find: [_.id], +// where: [[_.id, _._, _._]] +// })(todos); + +// let subs = new WeakMap(); + +// function makeTodoLi(ul, id) { +// let txt = document.createTextNode(""), +// li = document.createElement("li"), +// deleteButton = document.createElement("button"), +// viewer = view({ +// find: [_.status, _.text], +// use: [_.id], +// where: [ +// [_.id, "todo/status", _.status], +// [_.id, "todo/text", _.text] +// ] +// })(todos, id), +// updater = watch((added) => { +// if (added.size()) { +// let [[status, text]] = added; +// txt.textContent = text; +// } +// })(viewer); +// li.appendChild(txt); +// li.dataset.todoId = id; +// deleteButton.innerText = "delete"; +// li.appendChild(deleteButton); +// ul.appendChild(li); +// subs.set(li, [viewer, updater]); +// } + +// function destroyTodoLi(ul, id) { +// let li = ul.querySelector(`li[data-todo-id='${id}']`) +// subs.get(li).forEach(x => x.detach()); +// ul.removeChild(li); +// } + +// document.addEventListener("DOMContentLoaded", () => { +// let ul = document.getElementById("todos-ul"); +// watch((added, removed) => { +// for (let [id] of added) makeTodoLi(ul, id); +// for (let [id] of removed) destroyTodoLi(ul, id); +// })(todoIds); +// }); +// function findTodoLi(ul, id) { +// for (let li of ul.children) { +// if (li.dataset.id == id) { +// return li; +// } +// } +// } + +// let showStatus = input(ANY.VALUE); +// // let showStatus = input("ready"); + +// let visibleTodos = view({ +// find: [_.id, _.status, _.text], +// use: [_.status], +// where: [ +// [_.id, "todo/text", _.text], +// [_.id, "todo/status", _.status] +// ] +// })(todos, showStatus); + +// let todoTexts = view({ +// find: [_.id, _.text], +// where: [[_.id, _._, _.text]] +// })(visibleTodos); + +// let todoStatuses = view({ +// find: [_.id, _.status], +// where: [[_.id, _.status, _._]] +// })(visibleTodos); + +// document.addEventListener("DOMContentLoaded", () => { +// let ul = document.getElementById("todos-ul"), +// input = document.getElementById("todos-new"), +// add = document.getElementById("todos-add"); +// add.addEventListener("click", () => { +// addTodo(input.value); +// input.value = ""; +// }); +// watch((added, removed) => { +// for (let [id, text] of added) { +// let li = findTodoLi(ul, id); +// if (!li) { +// li = document.createElement("li"); +// li.appendChild(document.createTextNode(text)); +// li.dataset.id = id; +// li.addEventListener("click", () => removeTodo(id)); +// ul.appendChild(li); +// } +// } +// for (let [id] of removed) { +// for (let child of ul.children) { +// if (child.dataset.id == id) { +// ul.removeChild(child); +// } +// } +// } +// })(todoTexts); +// });