2019-06-13 00:41:45 +02:00
|
|
|
import { setAttribute, text, TAG_NAMES } from "./html.js";
|
|
|
|
|
|
|
|
/*
|
|
|
|
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;
|
2019-06-14 22:43:31 +02:00
|
|
|
this._eventListeners = null;
|
|
|
|
this._bindings = null;
|
|
|
|
this._subTemplates = null;
|
|
|
|
this._root = render(this, this._value);
|
|
|
|
this._attach();
|
2019-06-13 00:41:45 +02:00
|
|
|
}
|
|
|
|
|
2019-06-14 22:43:31 +02:00
|
|
|
// TODO: obj needs to support bindings
|
|
|
|
classes(obj) {
|
2019-06-13 00:41:45 +02:00
|
|
|
Object.entries(obj).filter(([, value]) => value).map(([key]) => key).join(" ");
|
|
|
|
}
|
|
|
|
|
|
|
|
root() {
|
|
|
|
return this._root;
|
|
|
|
}
|
|
|
|
|
|
|
|
update(value) {
|
|
|
|
this._value = value;
|
2019-06-14 22:43:31 +02:00
|
|
|
if (this._bindings) {
|
|
|
|
for (const binding of this._bindings) {
|
|
|
|
binding();
|
|
|
|
}
|
2019-06-13 00:41:45 +02:00
|
|
|
}
|
2019-06-14 22:43:31 +02:00
|
|
|
if (this._subTemplates) {
|
|
|
|
for (const sub of this._subTemplates) {
|
|
|
|
sub.update(value);
|
|
|
|
}
|
2019-06-13 00:41:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-14 22:43:31 +02:00
|
|
|
dispose() {
|
|
|
|
if (this._eventListeners) {
|
|
|
|
for (let {node, name, fn} of this._eventListeners) {
|
|
|
|
node.removeEventListener(name, fn);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this._subTemplates) {
|
|
|
|
for (const sub of this._subTemplates) {
|
|
|
|
sub.dispose();
|
|
|
|
}
|
2019-06-13 00:41:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-14 22:43:31 +02:00
|
|
|
_attach() {
|
|
|
|
if (this._eventListeners) {
|
|
|
|
for (let {node, name, fn} of this._eventListeners) {
|
|
|
|
node.addEventListener(name, fn);
|
|
|
|
}
|
|
|
|
}
|
2019-06-13 00:41:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
_addEventListener(node, name, fn) {
|
2019-06-14 22:43:31 +02:00
|
|
|
if (!this._eventListeners) {
|
|
|
|
this._eventListeners = [];
|
|
|
|
}
|
2019-06-13 00:41:45 +02:00
|
|
|
this._eventListeners.push({node, name, fn});
|
|
|
|
}
|
|
|
|
|
2019-06-14 22:43:31 +02:00
|
|
|
_addBinding(bindingFn) {
|
|
|
|
if (!this._bindings) {
|
|
|
|
this._bindings = [];
|
|
|
|
}
|
|
|
|
this._bindings.push(bindingFn);
|
|
|
|
}
|
|
|
|
|
|
|
|
_addSubTemplate(t) {
|
|
|
|
if (!this._subTemplates) {
|
|
|
|
this._subTemplates = [];
|
|
|
|
}
|
|
|
|
this._subTemplates.push(t);
|
|
|
|
}
|
|
|
|
|
|
|
|
_addAttributeBinding(node, name, fn) {
|
2019-06-13 00:41:45 +02:00
|
|
|
let prevValue = undefined;
|
|
|
|
const binding = () => {
|
|
|
|
const newValue = fn(this._value);
|
|
|
|
if (prevValue !== newValue) {
|
|
|
|
prevValue = newValue;
|
|
|
|
setAttribute(node, name, newValue);
|
|
|
|
}
|
|
|
|
};
|
2019-06-14 22:43:31 +02:00
|
|
|
this._addBinding(binding);
|
2019-06-13 00:41:45 +02:00
|
|
|
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+"";
|
|
|
|
}
|
|
|
|
};
|
2019-06-14 22:43:31 +02:00
|
|
|
|
|
|
|
this._addBinding(binding);
|
2019-06-13 00:41:45 +02:00
|
|
|
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
|
2019-06-14 22:43:31 +02:00
|
|
|
if (typeof attributes !== "object" || !!attributes.nodeType || Array.isArray(attributes)) {
|
2019-06-13 00:41:45 +02:00
|
|
|
children = attributes;
|
|
|
|
attributes = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const node = document.createElement(name);
|
|
|
|
|
|
|
|
if (attributes) {
|
|
|
|
for(let [key, value] of Object.entries(attributes)) {
|
|
|
|
const isFn = typeof value === "function";
|
2019-06-14 22:41:50 +02:00
|
|
|
if (key.startsWith("on") && key.length > 2 && isFn) {
|
2019-06-13 00:41:45 +02:00
|
|
|
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) {
|
2019-06-14 22:43:31 +02:00
|
|
|
if (typeof child === "function") {
|
2019-06-13 00:41:45 +02:00
|
|
|
child = this._addTextBinding(child);
|
2019-06-14 22:43:31 +02:00
|
|
|
} else if (!child.nodeType) {
|
|
|
|
// not a DOM node, turn into text
|
|
|
|
child = text(child);
|
2019-06-13 00:41:45 +02:00
|
|
|
}
|
|
|
|
node.appendChild(child);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return node;
|
|
|
|
}
|
2019-06-14 22:43:31 +02:00
|
|
|
|
|
|
|
_addReplaceNodeBinding(fn, renderNode) {
|
|
|
|
let prevValue = fn(this._value);
|
|
|
|
let node = renderNode(null);
|
|
|
|
|
|
|
|
const binding = () => {
|
|
|
|
const newValue = fn(this._value);
|
|
|
|
if (prevValue !== newValue) {
|
|
|
|
prevValue = newValue;
|
|
|
|
const newNode = renderNode(node);
|
|
|
|
if (node.parentElement) {
|
|
|
|
node.parentElement.replaceChild(newNode, node);
|
|
|
|
}
|
|
|
|
node = newNode;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this._addBinding(binding);
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
// creates a conditional subtemplate
|
|
|
|
if(fn, render) {
|
|
|
|
const boolFn = value => !!fn(value);
|
|
|
|
return this._addReplaceNodeBinding(boolFn, (prevNode) => {
|
|
|
|
if (prevNode && prevNode.nodeType !== Node.COMMENT_NODE) {
|
|
|
|
const templateIdx = this._subTemplates.findIndex(t => t.root() === prevNode);
|
|
|
|
const [template] = this._subTemplates.splice(templateIdx, 1);
|
|
|
|
template.dispose();
|
|
|
|
}
|
|
|
|
if (boolFn(this._value)) {
|
|
|
|
const template = new Template(this._value, render);
|
|
|
|
this._addSubTemplate(template);
|
|
|
|
return template.root();
|
|
|
|
} else {
|
|
|
|
return document.createComment("if placeholder");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2019-06-13 00:41:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for (const tag of TAG_NAMES) {
|
|
|
|
Template.prototype[tag] = function(...params) {
|
2019-06-14 22:43:31 +02:00
|
|
|
return this.el(tag, ... params);
|
2019-06-13 00:41:45 +02:00
|
|
|
};
|
|
|
|
}
|
2019-06-14 22:43:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
|