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.bz_;
var VComment = vdom.bA_;
var VDocumentFragment = vdom.bB_;
var VText = vdom.bC_;
var VComponent = vdom.bD_;
var VFragment = vdom.bE_;
var virtualizeHTML = vdom.bF_;
var EVENT_UPDATE = "update";
var EVENT_FINISH = "finish";
function State(tree) {
this.bG_ = new EventEmitter();
this.bH_ = tree;
this.bI_ = false;
}
function AsyncVDOMBuilder(globalData, parentNode, parentOut) {
if (!parentNode) {
parentNode = new VDocumentFragment();
}
var state;
if (parentOut) {
state = parentOut.A_;
} else {
state = new State(parentNode);
}
this.bJ_ = 1;
this.bK_ = 0;
this.bL_ = null;
this.bM_ = parentOut;
this.data = {};
this.A_ = state;
this.ad_ = parentNode;
this.global = globalData || {};
this.bN_ = [parentNode];
this.bO_ = false;
this.bP_ = undefined;
this.b_ = null;
this.ab_ = null;
this.ac_ = null;
this.be_ = null;
}
var proto = AsyncVDOMBuilder.prototype = {
bv_: true,
C_: typeof document === "object" && document,
bc: function (component, key, ownerComponent) {
var vComponent = new VComponent(component, key, ownerComponent);
return this.bQ_(vComponent, 0, true);
},
bf_: function (component, key, ownerComponent) {
var vComponent = new VComponent(component, key, ownerComponent, true);
this.bQ_(vComponent, 0);
},
bQ_: function (child, childCount, pushToStack) {
this.ad_.bR_(child);
if (pushToStack === true) {
this.bN_.push(child);
this.ad_ = 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.bQ_(element, childCount);
},
bo_: 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.bS_();
this.node(clone);
clone._O_ = component;
return this;
},
node: function (node) {
this.ad_.bR_(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.ad_.bR_(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.bQ_(element, childCount, true);
return this;
},
bm_: 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.bQ_(fragment, null, true);
return this;
},
ef: function () {
this.endElement();
},
endElement: function () {
var stack = this.bN_;
stack.pop();
this.ad_ = stack[stack.length - 1];
},
end: function () {
this.ad_ = undefined;
var remaining = --this.bJ_;
var parentOut = this.bM_;
if (remaining === 0) {
if (parentOut) {
parentOut.bT_();
} else {
this.bU_();
}
} else if (remaining - this.bK_ === 0) {
this.bV_();
}
return this;
},
bT_: function () {
var remaining = --this.bJ_;
if (remaining === 0) {
var parentOut = this.bM_;
if (parentOut) {
parentOut.bT_();
} else {
this.bU_();
}
} else if (remaining - this.bK_ === 0) {
this.bV_();
}
},
bU_: function () {
var state = this.A_;
state.bI_ = true;
state.bG_.emit(EVENT_FINISH, this.n_());
},
bV_: 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.bO_) {
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.A_;
if (options) {
if (options.last) {
this.bK_++;
}
}
this.bJ_++;
var documentFragment = this.ad_.bW_();
var asyncOut = new AsyncVDOMBuilder(this.global, documentFragment, this);
state.bG_.emit("beginAsync", {
out: asyncOut,
parentOut: this
});
return asyncOut;
},
createOut: function () {
return new AsyncVDOMBuilder(this.global);
},
flush: function () {
var events = this.A_.bG_;
if (events.listenerCount(EVENT_UPDATE)) {
events.emit(EVENT_UPDATE, new RenderResult(this));
}
},
aj_: function () {
return this.A_.bH_;
},
n_: function () {
return this.bX_ || (this.bX_ = new RenderResult(this));
},
on: function (event, callback) {
var state = this.A_;
if (event === EVENT_FINISH && state.bI_) {
callback(this.n_());
} else if (event === "last") {
this.onLast(callback);
} else {
state.bG_.on(event, callback);
}
return this;
},
once: function (event, callback) {
var state = this.A_;
if (event === EVENT_FINISH && state.bI_) {
callback(this.n_());
} else if (event === "last") {
this.onLast(callback);
} else {
state.bG_.once(event, callback);
}
return this;
},
emit: function (type, arg) {
var events = this.A_.bG_;
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.A_.bG_;
events.removeListener.apply(events, arguments);
return this;
},
sync: function () {
this.bO_ = true;
},
isSync: function () {
return this.bO_;
},
onLast: function (callback) {
var lastArray = this._last;
if (lastArray === undefined) {
this._last = [callback];
} else {
lastArray.push(callback);
}
return this;
},
ai_: function (host) {
var node = this.bP_;
if (!node) {
var vdomTree = this.aj_();
if (!host) host = this.C_;
this.bP_ = node = vdomTree.br_(host, null);
morphdom(node, vdomTree, host, this.b_);
}
return node;
},
toString: function (host) {
var docFragment = this.ai_(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.ab_ = componentDef;
this.ac_ = key;
this.be_ = customEvents;
}
};
proto.e = proto.element;
proto.be = proto.beginElement;
proto.ee = proto.bn_ = proto.endElement;
proto.t = proto.text;
proto.h = proto.w = proto.write = proto.html;
module.exports = AsyncVDOMBuilder;