forked from mystiq/hydrogen-web
Template becomes a view
This commit is contained in:
parent
657ec9aa62
commit
2008cf74f1
3 changed files with 103 additions and 78 deletions
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue