forked from mystiq/hydrogen-web
more work on databinding and templating
This commit is contained in:
parent
c7163a0554
commit
eb2eb291d3
5 changed files with 190 additions and 142 deletions
|
@ -1,29 +1,12 @@
|
||||||
import { li } from "./html.js";
|
import TemplateView from "./TemplateView.js";
|
||||||
|
|
||||||
export default class RoomTile {
|
export default class RoomTile extends TemplateView {
|
||||||
constructor(viewModel) {
|
render(t) {
|
||||||
this._viewModel = viewModel;
|
return t.li(vm => vm.name);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// called from ListView
|
||||||
clicked() {
|
clicked() {
|
||||||
this._viewModel.open();
|
this._viewModel.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
root() {
|
|
||||||
return this._root;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
151
src/ui/web/Template.js
Normal file
151
src/ui/web/Template.js
Normal file
|
@ -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);
|
||||||
|
};
|
||||||
|
}
|
29
src/ui/web/TemplateView.js
Normal file
29
src/ui/web/TemplateView.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,11 @@ export function text(str) {
|
||||||
return document.createTextNode(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 ol(... params) { return el("ol", ... params); }
|
||||||
export function ul(... params) { return el("ul", ... params); }
|
export function ul(... params) { return el("ul", ... params); }
|
||||||
export function li(... params) { return el("li", ... params); }
|
export function li(... params) { return el("li", ... params); }
|
||||||
|
|
|
@ -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); }
|
|
||||||
}
|
|
Loading…
Reference in a new issue