From eb2eb291d3271b76227ba611e69549807990df24 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 13 Jun 2019 00:41:45 +0200 Subject: [PATCH] more work on databinding and templating --- src/ui/web/RoomTile.js | 27 ++----- src/ui/web/Template.js | 151 +++++++++++++++++++++++++++++++++++++ src/ui/web/TemplateView.js | 29 +++++++ src/ui/web/html.js | 5 ++ src/ui/web/tree.js | 120 ----------------------------- 5 files changed, 190 insertions(+), 142 deletions(-) create mode 100644 src/ui/web/Template.js create mode 100644 src/ui/web/TemplateView.js delete mode 100644 src/ui/web/tree.js diff --git a/src/ui/web/RoomTile.js b/src/ui/web/RoomTile.js index 4beb6f1a..a72f6745 100644 --- a/src/ui/web/RoomTile.js +++ b/src/ui/web/RoomTile.js @@ -1,29 +1,12 @@ -import { li } from "./html.js"; +import TemplateView from "./TemplateView.js"; -export default class RoomTile { - constructor(viewModel) { - this._viewModel = viewModel; - this._root = null; - } - - mount() { - this._root = li(null, this._viewModel.name); - return this._root; - } - - unmount() { - } - - update() { - // no data-binding yet - this._root.innerText = this._viewModel.name; +export default class RoomTile extends TemplateView { + render(t) { + return t.li(vm => vm.name); } + // called from ListView clicked() { this._viewModel.open(); } - - root() { - return this._root; - } } diff --git a/src/ui/web/Template.js b/src/ui/web/Template.js new file mode 100644 index 00000000..3150f172 --- /dev/null +++ b/src/ui/web/Template.js @@ -0,0 +1,151 @@ +import { setAttribute, text, TAG_NAMES } from "./html.js"; + +// const template = new Template(vm, t => { +// return t.div({onClick: () => this._clicked(), className: "errorLabel"}, [ +// vm => vm.label, +// t.span({className: vm => t.className({fatal: !!vm.fatal})}, [vm => vm.error]) +// ]); +// }); + +/* + supports + - event handlers (attribute fn value with name that starts with on) + - one way binding of attributes (other attribute fn value) + - one way binding of text values (child fn value) + - refs to get dom nodes + - className binding returning object with className => enabled map + missing: + - create views +*/ +export default class Template { + constructor(value, render) { + this._value = value; + this._refs = {}; + this._eventListeners = []; + this._bindings = []; + this._render = render; + } + + className(obj) { + Object.entries(obj).filter(([, value]) => value).map(([key]) => key).join(" "); + } + + root() { + if (!this._root) { + this._root = this._render(this, this._value); + } + return this._root; + } + + ref(name) { + return this._refs[name]; + } + + update(value) { + this._value = value; + for (const binding of this._bindings) { + binding(); + } + } + + detach() { + for (let {node, name, fn} of this._eventListeners) { + node.removeEventListener(name, fn); + } + } + + attach() { + for (let {node, name, fn} of this._eventListeners) { + node.addEventListener(name, fn); + } + } + + _addRef(name, node) { + this._refs[name] = node; + } + + _addEventListener(node, name, fn) { + this._eventListeners.push({node, name, fn}); + } + + _setAttributeBinding(node, name, fn) { + let prevValue = undefined; + const binding = () => { + const newValue = fn(this._value); + if (prevValue !== newValue) { + prevValue = newValue; + setAttribute(node, name, newValue); + } + }; + this._bindings.push(binding); + binding(); + } + + _addTextBinding(fn) { + const initialValue = fn(this._value); + const node = text(initialValue); + let prevValue = initialValue; + const binding = () => { + const newValue = fn(this._value); + if (prevValue !== newValue) { + prevValue = newValue; + node.textContent = newValue+""; + } + }; + this._bindings.push(binding); + return node; + } + + el(name, attributes, children) { + if (attributes) { + // valid attributes is only object that is not a DOM node + // anything else (string, fn, array, dom node) is presumed + // to be children with no attributes passed + if (typeof attributes !== "object" || attributes.nodeType === Node.ELEMENT_NODE || Array.isArray(attributes)) { + children = attributes; + attributes = null; + } + } + + const node = document.createElement(name); + + if (attributes) { + for(let [key, value] of Object.entries(attributes)) { + const isFn = typeof value === "function"; + if (key === "ref") { + this._refs[value] = node; + } else if (key.startsWith("on") && key.length > 2 && isFn) { + const eventName = key.substr(2, 1).toLowerCase() + key.substr(3); + const handler = value; + this._addEventListener(node, eventName, handler); + } else if (isFn) { + this._addAttributeBinding(node, key, value); + } else { + setAttribute(node, key, value); + } + } + } + + if (children) { + if (!Array.isArray(children)) { + children = [children]; + } + for (let child of children) { + if (typeof child === "string") { + child = text(child); + } else if (typeof c === "function") { + child = this._addTextBinding(child); + } + node.appendChild(child); + } + } + + return node; + } +} + +for (const tag of TAG_NAMES) { + Template.prototype[tag] = function(...params) { + this.el(tag, ... params); + }; +} diff --git a/src/ui/web/TemplateView.js b/src/ui/web/TemplateView.js new file mode 100644 index 00000000..f0a16cf0 --- /dev/null +++ b/src/ui/web/TemplateView.js @@ -0,0 +1,29 @@ +import Template from "./Template.js"; + +export default class TemplateView { + constructor(value) { + this._template = new Template(value, (t, value) => this.render(t, value)); + } + + render() { + throw new Error("render not implemented"); + } + + mount() { + const root = this._template.root(); + this._template.attach(); + return root; + } + + root() { + return this._template.root(); + } + + unmount() { + this._template.detach(); + } + + update(value) { + this._template.update(value); + } +} diff --git a/src/ui/web/html.js b/src/ui/web/html.js index ebf57091..e6b8a194 100644 --- a/src/ui/web/html.js +++ b/src/ui/web/html.js @@ -31,6 +31,11 @@ export function text(str) { return document.createTextNode(str); } +export const TAG_NAMES = [ + "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", + "p", "strong", "em", "span", "img", "section", "main", "article", "aside", + "pre", "button"]; + export function ol(... params) { return el("ol", ... params); } export function ul(... params) { return el("ul", ... params); } export function li(... params) { return el("li", ... params); } diff --git a/src/ui/web/tree.js b/src/ui/web/tree.js deleted file mode 100644 index 6e3ce694..00000000 --- a/src/ui/web/tree.js +++ /dev/null @@ -1,120 +0,0 @@ -import { setAttribute, addChildren, text } from "./html.js"; - -function renderTree() {} - - -const tree = renderTree(vm, t => { - return t.div({onClick: () => this._clicked(), className: "errorLabel"}, [ - vm => vm.label, - t.span({className: vm => {{fatal: !!vm.fatal}}}, [vm => vm.error]) - ]); -}); - -tree.root -tree.detach() -tree.updateBindings(vm); - - - - -class Tree { - constructor(value, render) { - this._ctx = new TreeContext(value); - this._root = render(this._ctx); - } - - ref(name) { - return this._ctx._refs[name]; - } - - detach() { - for (let {node, name, fn} of this._ctx._eventListeners) { - node.removeEventListener(name, fn); - } - } -} - -class TreeContext { - constructor(value) { - this._value = value; - this._refs = {}; - this._eventListeners = []; - this._bindings = []; - } - - _addRef(name, node) { - this._refs[name] = node; - } - - _addEventListener(node, name, fn) { - node.addEventListener(name, fn); - this._eventListeners.push({node, event, fn}); - } - - _setAttributeBinding(node, name, fn) { - let prevValue = undefined; - const binding = () => { - const newValue = fn(this._value); - if (prevValue !== newValue) { - prevValue = newValue; - setAttribute(node, name, newValue); - } - }; - this._bindings.push(binding); - binding(); - } - - _setTextBinding(fn) { - const initialValue = fn(this._value); - const node = text(initialValue); - let prevValue = initialValue; - const binding = () => { - const newValue = fn(this._value); - if (prevValue !== newValue) { - prevValue = newValue; - node.textContent = newValue+""; - } - }; - this._bindings.push(binding); - } - - el(name, attributes, children) { - const node = document.createElement(name); - for(let [key, value] of Object.entries(attributes)) { - const isFn = typeof value === "function"; - if (key.startsWith("on") && key.length > 2 && isFn) { - const eventName = key.substr(2, 1).toLowerCase() + key.substr(3); - const handler = value; - this._addEventListener(node, eventName, handler); - } else if (isFn) { - this._addAttributeBinding(node, key, value); - } else { - setAttribute(node, key, value); - } - } - - addChildren(node, children); - return node; - } - - ol(... params) { return this.el("ol", ... params); } - ul(... params) { return this.el("ul", ... params); } - li(... params) { return this.el("li", ... params); } - div(... params) { return this.el("div", ... params); } - h1(... params) { return this.el("h1", ... params); } - h2(... params) { return this.el("h2", ... params); } - h3(... params) { return this.el("h3", ... params); } - h4(... params) { return this.el("h4", ... params); } - h5(... params) { return this.el("h5", ... params); } - h6(... params) { return this.el("h6", ... params); } - p(... params) { return this.el("p", ... params); } - strong(... params) { return this.el("strong", ... params); } - em(... params) { return this.el("em", ... params); } - span(... params) { return this.el("span", ... params); } - img(... params) { return this.el("img", ... params); } - section(... params) { return this.el("section", ... params); } - main(... params) { return this.el("main", ... params); } - article(... params) { return this.el("article", ... params); } - aside(... params) { return this.el("aside", ... params); } - pre(... params) { return this.el("pre", ... params); } -}