Template becomes a view

This commit is contained in:
Bruno Windels 2020-04-29 10:00:51 +02:00
parent 657ec9aa62
commit 2008cf74f1
3 changed files with 103 additions and 78 deletions

View file

@ -10,6 +10,8 @@ function insertAt(parentNode, idx, childNode) {
} }
} }
const MOUNT_ARGS = {parentProvidesUpdates: true};
export class ListView { export class ListView {
constructor({list, onItemClick, className}, childCreator) { constructor({list, onItemClick, className}, childCreator) {
this._onItemClick = onItemClick; this._onItemClick = onItemClick;
@ -86,7 +88,7 @@ export class ListView {
for (let item of this._list) { for (let item of this._list) {
const child = this._childCreator(item); const child = this._childCreator(item);
this._childInstances.push(child); this._childInstances.push(child);
const childDomNode = child.mount(); const childDomNode = child.mount(MOUNT_ARGS);
this._root.appendChild(childDomNode); this._root.appendChild(childDomNode);
} }
} }
@ -95,7 +97,7 @@ export class ListView {
this.onBeforeListChanged(); this.onBeforeListChanged();
const child = this._childCreator(value); const child = this._childCreator(value);
this._childInstances.splice(idx, 0, child); this._childInstances.splice(idx, 0, child);
insertAt(this._root, idx, child.mount()); insertAt(this._root, idx, child.mount(MOUNT_ARGS));
this.onListChanged(); this.onListChanged();
} }

View file

@ -1,5 +1,5 @@
import { setAttribute, text, isChildren, classNames, TAG_NAMES } from "./html.js"; import { setAttribute, text, isChildren, classNames, TAG_NAMES } from "./html.js";
import {errorToDOM} from "./error.js";
function objHasFns(obj) { function objHasFns(obj) {
for(const value of Object.values(obj)) { for(const value of Object.values(obj)) {
@ -23,43 +23,39 @@ function objHasFns(obj) {
- create views - create views
*/ */
export class Template { export class Template {
constructor(value, render) { constructor(value, render = undefined) {
this._value = value; this._value = value;
this._render = render;
this._eventListeners = null; this._eventListeners = null;
this._bindings = null; this._bindings = null;
this._subTemplates = null; // this should become _subViews and also include templates.
this._root = render(this, this._value); // How do we know which ones we should update though?
this._attach(); // Wrapper class?
this._subViews = null;
this._root = null;
this._boundUpdateFromValue = null;
} }
root() { _subscribe() {
return this._root; this._boundUpdateFromValue = this._updateFromValue.bind(this);
}
update(value) { if (typeof this._value.on === "function") {
this._value = value; this._value.on("change", this._boundUpdateFromValue);
if (this._bindings) {
for (const binding of this._bindings) {
binding();
}
} }
if (this._subTemplates) { else if (typeof this._value.subscribe === "function") {
for (const sub of this._subTemplates) { this._value.subscribe(this._boundUpdateFromValue);
sub.update(value);
}
} }
} }
dispose() { _unsubscribe() {
if (this._eventListeners) { if (this._boundUpdateFromValue) {
for (let {node, name, fn} of this._eventListeners) { if (typeof this._value.off === "function") {
node.removeEventListener(name, fn); this._value.off("change", this._boundUpdateFromValue);
} }
} else if (typeof this._value.unsubscribe === "function") {
if (this._subTemplates) { this._value.unsubscribe(this._boundUpdateFromValue);
for (const sub of this._subTemplates) {
sub.dispose();
} }
this._boundUpdateFromValue = null;
} }
} }
@ -71,6 +67,53 @@ export class Template {
} }
} }
_detach() {
if (this._eventListeners) {
for (let {node, name, fn} of this._eventListeners) {
node.removeEventListener(name, fn);
}
}
}
mount(options) {
if (this._render) {
this._root = this._render(this, this._value);
} else if (this.render) { // overriden in subclass
this._root = this.render(this, this._value);
}
const parentProvidesUpdates = options && options.parentProvidesUpdates;
if (!parentProvidesUpdates) {
this._subscribe();
}
this._attach();
return this._root;
}
unmount() {
this._detach();
this._unsubscribe();
for (const v of this._subViews) {
v.unmount();
}
}
root() {
return this._root;
}
_updateFromValue() {
this.update(this._value);
}
update(value) {
this._value = value;
if (this._bindings) {
for (const binding of this._bindings) {
binding();
}
}
}
_addEventListener(node, name, fn) { _addEventListener(node, name, fn) {
if (!this._eventListeners) { if (!this._eventListeners) {
this._eventListeners = []; this._eventListeners = [];
@ -85,11 +128,11 @@ export class Template {
this._bindings.push(bindingFn); this._bindings.push(bindingFn);
} }
_addSubTemplate(t) { _addSubView(view) {
if (!this._subTemplates) { if (!this._subViews) {
this._subTemplates = []; this._subViews = [];
} }
this._subTemplates.push(t); this._subViews.push(view);
} }
_addAttributeBinding(node, name, fn) { _addAttributeBinding(node, name, fn) {
@ -199,19 +242,38 @@ export class Template {
return node; return node;
} }
// this insert a view, and is not a view factory for `if`, so returns the root element to insert in the template
// you should not call t.view() and not use the result (e.g. attach the result to the template DOM tree).
view(view) {
let root;
try {
root = view.mount();
} catch (err) {
return errorToDOM(err);
}
this._addSubView(view);
return root;
}
// sugar
createTemplate(render) {
return vm => new Template(vm, render);
}
// creates a conditional subtemplate // creates a conditional subtemplate
if(fn, render) { if(fn, viewCreator) {
const boolFn = value => !!fn(value); const boolFn = value => !!fn(value);
return this._addReplaceNodeBinding(boolFn, (prevNode) => { return this._addReplaceNodeBinding(boolFn, (prevNode) => {
if (prevNode && prevNode.nodeType !== Node.COMMENT_NODE) { if (prevNode && prevNode.nodeType !== Node.COMMENT_NODE) {
const templateIdx = this._subTemplates.findIndex(t => t.root() === prevNode); const viewIdx = this._subViews.findIndex(v => v.root() === prevNode);
const [template] = this._subTemplates.splice(templateIdx, 1); if (viewIdx !== -1) {
template.dispose(); const [view] = this._subViews.splice(viewIdx, 1);
view.unmount();
}
} }
if (boolFn(this._value)) { if (boolFn(this._value)) {
const template = new Template(this._value, render); const view = viewCreator(this._value);
this._addSubTemplate(template); return this.view(view);
return template.root();
} else { } else {
return document.createComment("if placeholder"); return document.createComment("if placeholder");
} }

View file

@ -1,39 +0,0 @@
import {Template} from "./Template.js";
export class TemplateView {
constructor(vm, bindToChangeEvent) {
this.viewModel = vm;
this._changeEventHandler = bindToChangeEvent ? this.update.bind(this, this.viewModel) : null;
this._template = null;
}
render() {
throw new Error("render not implemented");
}
mount() {
if (this._changeEventHandler) {
this.viewModel.on("change", this._changeEventHandler);
}
this._template = new Template(this.viewModel, (t, value) => this.render(t, value));
return this.root();
}
root() {
return this._template.root();
}
unmount() {
if (this._changeEventHandler) {
this.viewModel.off("change", this._changeEventHandler);
}
this._template.dispose();
this._template = null;
}
update(value, prop) {
if (this._template) {
this._template.update(value);
}
}
}