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 {
constructor({list, onItemClick, className}, childCreator) {
this._onItemClick = onItemClick;
@ -86,7 +88,7 @@ export class ListView {
for (let item of this._list) {
const child = this._childCreator(item);
this._childInstances.push(child);
const childDomNode = child.mount();
const childDomNode = child.mount(MOUNT_ARGS);
this._root.appendChild(childDomNode);
}
}
@ -95,7 +97,7 @@ export class ListView {
this.onBeforeListChanged();
const child = this._childCreator(value);
this._childInstances.splice(idx, 0, child);
insertAt(this._root, idx, child.mount());
insertAt(this._root, idx, child.mount(MOUNT_ARGS));
this.onListChanged();
}

View file

@ -1,5 +1,5 @@
import { setAttribute, text, isChildren, classNames, TAG_NAMES } from "./html.js";
import {errorToDOM} from "./error.js";
function objHasFns(obj) {
for(const value of Object.values(obj)) {
@ -23,43 +23,39 @@ function objHasFns(obj) {
- create views
*/
export class Template {
constructor(value, render) {
constructor(value, render = undefined) {
this._value = value;
this._render = render;
this._eventListeners = null;
this._bindings = null;
this._subTemplates = null;
this._root = render(this, this._value);
this._attach();
// this should become _subViews and also include templates.
// How do we know which ones we should update though?
// Wrapper class?
this._subViews = null;
this._root = null;
this._boundUpdateFromValue = null;
}
root() {
return this._root;
}
_subscribe() {
this._boundUpdateFromValue = this._updateFromValue.bind(this);
update(value) {
this._value = value;
if (this._bindings) {
for (const binding of this._bindings) {
binding();
}
if (typeof this._value.on === "function") {
this._value.on("change", this._boundUpdateFromValue);
}
if (this._subTemplates) {
for (const sub of this._subTemplates) {
sub.update(value);
}
else if (typeof this._value.subscribe === "function") {
this._value.subscribe(this._boundUpdateFromValue);
}
}
dispose() {
if (this._eventListeners) {
for (let {node, name, fn} of this._eventListeners) {
node.removeEventListener(name, fn);
_unsubscribe() {
if (this._boundUpdateFromValue) {
if (typeof this._value.off === "function") {
this._value.off("change", this._boundUpdateFromValue);
}
}
if (this._subTemplates) {
for (const sub of this._subTemplates) {
sub.dispose();
else if (typeof this._value.unsubscribe === "function") {
this._value.unsubscribe(this._boundUpdateFromValue);
}
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) {
if (!this._eventListeners) {
this._eventListeners = [];
@ -85,11 +128,11 @@ export class Template {
this._bindings.push(bindingFn);
}
_addSubTemplate(t) {
if (!this._subTemplates) {
this._subTemplates = [];
_addSubView(view) {
if (!this._subViews) {
this._subViews = [];
}
this._subTemplates.push(t);
this._subViews.push(view);
}
_addAttributeBinding(node, name, fn) {
@ -199,19 +242,38 @@ export class Template {
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
if(fn, render) {
if(fn, viewCreator) {
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();
const viewIdx = this._subViews.findIndex(v => v.root() === prevNode);
if (viewIdx !== -1) {
const [view] = this._subViews.splice(viewIdx, 1);
view.unmount();
}
}
if (boolFn(this._value)) {
const template = new Template(this._value, render);
this._addSubTemplate(template);
return template.root();
const view = viewCreator(this._value);
return this.view(view);
} else {
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);
}
}
}