marko
Version:
UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.
450 lines (370 loc) • 9.11 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 VDocumentFragment = vdom.bx_;
var VText = vdom.by_;
var VComponent = vdom.bz_;
var VFragment = vdom.bA_;
var virtualizeHTML = vdom.bB_;
var EVENT_UPDATE = "update";
var EVENT_FINISH = "finish";
function State(tree) {
this.bC_ = new EventEmitter();
this.bD_ = tree;
this.bE_ = false;
}
function AsyncVDOMBuilder(globalData, parentNode, parentOut) {
if (!parentNode) {
parentNode = new VDocumentFragment();
}
var state;
if (parentOut) {
state = parentOut.z_;
} else {
state = new State(parentNode);
}
this.bF_ = 1;
this.bG_ = 0;
this.bH_ = null;
this.bI_ = parentOut;
this.data = {};
this.z_ = state;
this.aa_ = parentNode;
this.global = globalData || {};
this.bJ_ = [parentNode];
this.bK_ = false;
this.bL_ = undefined;
this.b_ = null;
this._Z_ = null;
this.a__ = null;
this.ba_ = null;
}
var proto = AsyncVDOMBuilder.prototype = {
bq_: true,
B_: typeof document === "object" && document,
bc: function (component, key, ownerComponent) {
var vComponent = new VComponent(component, key, ownerComponent);
return this.bM_(vComponent, 0, true);
},
bb_: function (component, key, ownerComponent) {
var vComponent = new VComponent(component, key, ownerComponent, true);
this.bM_(vComponent, 0);
},
bM_: function (child, childCount, pushToStack) {
this.aa_.bN_(child);
if (pushToStack === true) {
this.bJ_.push(child);
this.aa_ = 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.bM_(element, childCount);
},
bk_: 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.bO_();
this.node(clone);
clone._L_ = component;
return this;
},
node: function (node) {
this.aa_.bN_(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.aa_.bN_(new VText(text, ownerComponent));
return this;
},
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.bM_(element, childCount, true);
return this;
},
bi_: 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.bM_(fragment, null, true);
return this;
},
ef: function () {
this.endElement();
},
endElement: function () {
var stack = this.bJ_;
stack.pop();
this.aa_ = stack[stack.length - 1];
},
end: function () {
this.aa_ = undefined;
var remaining = --this.bF_;
var parentOut = this.bI_;
if (remaining === 0) {
if (parentOut) {
parentOut.bP_();
} else {
this.bQ_();
}
} else if (remaining - this.bG_ === 0) {
this.bR_();
}
return this;
},
bP_: function () {
var remaining = --this.bF_;
if (remaining === 0) {
var parentOut = this.bI_;
if (parentOut) {
parentOut.bP_();
} else {
this.bQ_();
}
} else if (remaining - this.bG_ === 0) {
this.bR_();
}
},
bQ_: function () {
var state = this.z_;
state.bE_ = true;
state.bC_.emit(EVENT_FINISH, this.bs_());
},
bR_: 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.bK_) {
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.bG_++;
}
}
this.bF_++;
var documentFragment = this.aa_.bS_();
var asyncOut = new AsyncVDOMBuilder(this.global, documentFragment, this);
state.bC_.emit("beginAsync", {
out: asyncOut,
parentOut: this
});
return asyncOut;
},
createOut: function () {
return new AsyncVDOMBuilder(this.global);
},
flush: function () {
var events = this.z_.bC_;
if (events.listenerCount(EVENT_UPDATE)) {
events.emit(EVENT_UPDATE, new RenderResult(this));
}
},
ag_: function () {
return this.z_.bD_;
},
bs_: function () {
return this.bT_ || (this.bT_ = new RenderResult(this));
},
on: function (event, callback) {
var state = this.z_;
if (event === EVENT_FINISH && state.bE_) {
callback(this.bs_());
} else if (event === "last") {
this.onLast(callback);
} else {
state.bC_.on(event, callback);
}
return this;
},
once: function (event, callback) {
var state = this.z_;
if (event === EVENT_FINISH && state.bE_) {
callback(this.bs_());
} else if (event === "last") {
this.onLast(callback);
} else {
state.bC_.once(event, callback);
}
return this;
},
emit: function (type, arg) {
var events = this.z_.bC_;
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_.bC_;
events.removeListener.apply(events, arguments);
return this;
},
sync: function () {
this.bK_ = true;
},
isSync: function () {
return this.bK_;
},
onLast: function (callback) {
var lastArray = this._last;
if (lastArray === undefined) {
this._last = [callback];
} else {
lastArray.push(callback);
}
return this;
},
af_: function (host) {
var node = this.bL_;
if (!node) {
var vdomTree = this.ag_();
if (!host) host = this.B_;
this.bL_ = node = vdomTree.bm_(host, null);
morphdom(node, vdomTree, host, this.b_);
}
return node;
},
toString: function (host) {
var docFragment = this.af_(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) {
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._Z_ = componentDef;
this.a__ = key;
this.ba_ = customEvents;
}
};
proto.e = proto.element;
proto.be = proto.beginElement;
proto.ee = proto.bj_ = proto.endElement;
proto.t = proto.text;
proto.h = proto.w = proto.write = proto.html;
module.exports = AsyncVDOMBuilder;