marko
Version:
UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.
456 lines (375 loc) • 9.31 kB
JavaScript
"use strict";var EventEmitter = require("events-light");
var RenderResult = require("../RenderResult");
var attrsHelper = require("./helpers/attrs");
var morphdom = require("./morphdom");
var vdom = require("./vdom");
var VElement = vdom.bw_;
var VComment = vdom.bx_;
var VDocumentFragment = vdom.by_;
var VText = vdom.bz_;
var VComponent = vdom.bA_;
var VFragment = vdom.bB_;
var virtualizeHTML = vdom.bC_;
var EVENT_UPDATE = "update";
var EVENT_FINISH = "finish";
function State(tree) {
this.bD_ = new EventEmitter();
this.bE_ = tree;
this.bF_ = false;
}
function AsyncVDOMBuilder(globalData, parentNode, parentOut) {
if (!parentNode) {
parentNode = new VDocumentFragment();
}
var state;
if (parentOut) {
state = parentOut.z_;
} else {
state = new State(parentNode);
}
this.bG_ = 1;
this.bH_ = 0;
this.bI_ = null;
this.bJ_ = parentOut;
this.data = {};
this.z_ = state;
this.ab_ = parentNode;
this.global = globalData || {};
this.bK_ = [parentNode];
this.bL_ = false;
this.bM_ = undefined;
this.b_ = null;
this.a__ = null;
this.aa_ = null;
this.bc_ = null;
}
var proto = AsyncVDOMBuilder.prototype = {
bs_: true,
B_: typeof document === "object" && document,
bc: function (component, key, ownerComponent) {
var vComponent = new VComponent(component, key, ownerComponent);
return this.bN_(vComponent, 0, true);
},
bd_: function (component, key, ownerComponent) {
var vComponent = new VComponent(component, key, ownerComponent, true);
this.bN_(vComponent, 0);
},
bN_: function (child, childCount, pushToStack) {
this.ab_.bO_(child);
if (pushToStack === true) {
this.bK_.push(child);
this.ab_ = child;
}
return childCount === 0 ? this : child;
},
element: function (tagName, attrs, key, component, childCount, flags, props) {
var element = new VElement(
tagName,
attrs,
key,
component,
childCount,
flags,
props
);
return this.bN_(element, childCount);
},
bm_: function (tagName, attrs, key, componentDef, props) {
return this.element(
tagName,
attrsHelper(attrs),
key,
componentDef.s_,
0,
0,
props
);
},
n: function (node, component) {
// NOTE: We do a shallow clone since we assume the node is being reused
// and a node can only have one parent node.
var clone = node.bP_();
this.node(clone);
clone._M_ = component;
return this;
},
node: function (node) {
this.ab_.bO_(node);
return this;
},
text: function (text, ownerComponent) {
var type = typeof text;
if (type != "string") {
if (text == null) {
return;
} else if (type === "object") {
if (text.toHTML) {
return this.h(text.toHTML(), ownerComponent);
}
}
text = text.toString();
}
this.ab_.bO_(new VText(text, ownerComponent));
return this;
},
comment: function (comment, ownerComponent) {
return this.node(new VComment(comment, ownerComponent));
},
html: function (html, ownerComponent) {
if (html != null) {
var vdomNode = virtualizeHTML(html, ownerComponent);
this.node(vdomNode);
}
return this;
},
beginElement: function (
tagName,
attrs,
key,
component,
childCount,
flags,
props)
{
var element = new VElement(
tagName,
attrs,
key,
component,
childCount,
flags,
props
);
this.bN_(element, childCount, true);
return this;
},
bk_: function (tagName, attrs, key, componentDef, props) {
return this.beginElement(
tagName,
attrsHelper(attrs),
key,
componentDef.s_,
0,
0,
props
);
},
bf: function (key, component, preserve) {
var fragment = new VFragment(key, component, preserve);
this.bN_(fragment, null, true);
return this;
},
ef: function () {
this.endElement();
},
endElement: function () {
var stack = this.bK_;
stack.pop();
this.ab_ = stack[stack.length - 1];
},
end: function () {
this.ab_ = undefined;
var remaining = --this.bG_;
var parentOut = this.bJ_;
if (remaining === 0) {
if (parentOut) {
parentOut.bQ_();
} else {
this.bR_();
}
} else if (remaining - this.bH_ === 0) {
this.bS_();
}
return this;
},
bQ_: function () {
var remaining = --this.bG_;
if (remaining === 0) {
var parentOut = this.bJ_;
if (parentOut) {
parentOut.bQ_();
} else {
this.bR_();
}
} else if (remaining - this.bH_ === 0) {
this.bS_();
}
},
bR_: function () {
var state = this.z_;
state.bF_ = true;
state.bD_.emit(EVENT_FINISH, this.n_());
},
bS_: function () {
var lastArray = this._last;
var i = 0;
function next() {
if (i === lastArray.length) {
return;
}
var lastCallback = lastArray[i++];
lastCallback(next);
if (!lastCallback.length) {
next();
}
}
next();
},
error: function (e) {
try {
this.emit("error", e);
} finally {
// If there is no listener for the error event then it will
// throw a new Error here. In order to ensure that the async fragment
// is still properly ended we need to put the end() in a `finally`
// block
this.end();
}
return this;
},
beginAsync: function (options) {
if (this.bL_) {
throw Error(
"Tried to render async while in sync mode. Note: Client side await is not currently supported in re-renders (Issue: #942)."
);
}
var state = this.z_;
if (options) {
if (options.last) {
this.bH_++;
}
}
this.bG_++;
var documentFragment = this.ab_.bT_();
var asyncOut = new AsyncVDOMBuilder(this.global, documentFragment, this);
state.bD_.emit("beginAsync", {
out: asyncOut,
parentOut: this
});
return asyncOut;
},
createOut: function () {
return new AsyncVDOMBuilder(this.global);
},
flush: function () {
var events = this.z_.bD_;
if (events.listenerCount(EVENT_UPDATE)) {
events.emit(EVENT_UPDATE, new RenderResult(this));
}
},
ah_: function () {
return this.z_.bE_;
},
n_: function () {
return this.bU_ || (this.bU_ = new RenderResult(this));
},
on: function (event, callback) {
var state = this.z_;
if (event === EVENT_FINISH && state.bF_) {
callback(this.n_());
} else if (event === "last") {
this.onLast(callback);
} else {
state.bD_.on(event, callback);
}
return this;
},
once: function (event, callback) {
var state = this.z_;
if (event === EVENT_FINISH && state.bF_) {
callback(this.n_());
} else if (event === "last") {
this.onLast(callback);
} else {
state.bD_.once(event, callback);
}
return this;
},
emit: function (type, arg) {
var events = this.z_.bD_;
switch (arguments.length) {
case 1:
events.emit(type);
break;
case 2:
events.emit(type, arg);
break;
default:
events.emit.apply(events, arguments);
break;
}
return this;
},
removeListener: function () {
var events = this.z_.bD_;
events.removeListener.apply(events, arguments);
return this;
},
sync: function () {
this.bL_ = true;
},
isSync: function () {
return this.bL_;
},
onLast: function (callback) {
var lastArray = this._last;
if (lastArray === undefined) {
this._last = [callback];
} else {
lastArray.push(callback);
}
return this;
},
ag_: function (host) {
var node = this.bM_;
if (!node) {
var vdomTree = this.ah_();
if (!host) host = this.B_;
this.bM_ = node = vdomTree.bo_(host, null);
morphdom(node, vdomTree, host, this.b_);
}
return node;
},
toString: function (host) {
var docFragment = this.ag_(host);
var html = "";
var child = docFragment.firstChild;
while (child) {
var nextSibling = child.nextSibling;
if (child.nodeType != 1) {
var container = docFragment.ownerDocument.createElement("div");
container.appendChild(child.cloneNode());
html += container.innerHTML;
} else {
html += child.outerHTML;
}
child = nextSibling;
}
return html;
},
then: function (fn, fnErr) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
var out = this;
var promise = new Promise(function (resolve, reject) {
out.on("error", reject).on(EVENT_FINISH, function (result) {
resolve(result);
});
});
return Promise.resolve(promise).then(fn, fnErr);
},
catch: function (fnErr) {
return this.then(undefined, fnErr);
},
isVDOM: true,
c: function (componentDef, key, customEvents) {
this.a__ = componentDef;
this.aa_ = key;
this.bc_ = customEvents;
}
};
proto.e = proto.element;
proto.be = proto.beginElement;
proto.ee = proto.bl_ = proto.endElement;
proto.t = proto.text;
proto.h = proto.w = proto.write = proto.html;
module.exports = AsyncVDOMBuilder;