UNPKG

marko

Version:

UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.

457 lines (375 loc) • 10.3 kB
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.___VElement; var VComment = vdom.___VComment; var VDocumentFragment = vdom.___VDocumentFragment; var VText = vdom.___VText; var VComponent = vdom.___VComponent; var VFragment = vdom.___VFragment; var virtualizeHTML = vdom.___virtualizeHTML; var EVENT_UPDATE = "update"; var EVENT_FINISH = "finish"; function State(tree) { this.___events = new EventEmitter(); this.___tree = tree; this.___finished = false; } function AsyncVDOMBuilder(globalData, parentNode, parentOut) { if (!parentNode) { parentNode = new VDocumentFragment(); } var state; if (parentOut) { state = parentOut.___state; } else { state = new State(parentNode); } this.___remaining = 1; this.___lastCount = 0; this.___last = null; this.___parentOut = parentOut; this.data = {}; this.___state = state; this.___parent = parentNode; this.global = globalData || {}; this.___stack = [parentNode]; this.___sync = false; this.___vnode = undefined; this.___components = null; this.___assignedComponentDef = null; this.___assignedKey = null; this.___assignedCustomEvents = null; } var proto = (AsyncVDOMBuilder.prototype = { ___isOut: true, ___host: typeof document === "object" && document, bc: function (component, key, ownerComponent) { var vComponent = new VComponent(component, key, ownerComponent); return this.___beginNode(vComponent, 0, true); }, ___preserveComponent: function (component, key, ownerComponent) { var vComponent = new VComponent(component, key, ownerComponent, true); this.___beginNode(vComponent, 0); }, ___beginNode: function (child, childCount, pushToStack) { this.___parent.___appendChild(child); if (pushToStack === true) { this.___stack.push(child); this.___parent = 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.___beginNode(element, childCount); }, ___elementDynamic: function (tagName, attrs, key, componentDef, props) { return this.element( tagName, attrsHelper(attrs), key, componentDef.___component, 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.___cloneNode(); this.node(clone); clone.___ownerComponent = component; return this; }, node: function (node) { this.___parent.___appendChild(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.___parent.___appendChild(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.___beginNode(element, childCount, true); return this; }, ___beginElementDynamic: function (tagName, attrs, key, componentDef, props) { return this.beginElement( tagName, attrsHelper(attrs), key, componentDef.___component, 0, 0, props, ); }, bf: function (key, component, preserve) { var fragment = new VFragment(key, component, preserve); this.___beginNode(fragment, null, true); return this; }, ef: function () { this.endElement(); }, endElement: function () { var stack = this.___stack; stack.pop(); this.___parent = stack[stack.length - 1]; }, end: function () { this.___parent = undefined; var remaining = --this.___remaining; var parentOut = this.___parentOut; if (remaining === 0) { if (parentOut) { parentOut.___handleChildDone(); } else { this.___doFinish(); } } else if (remaining - this.___lastCount === 0) { this.___emitLast(); } return this; }, ___handleChildDone: function () { var remaining = --this.___remaining; if (remaining === 0) { var parentOut = this.___parentOut; if (parentOut) { parentOut.___handleChildDone(); } else { this.___doFinish(); } } else if (remaining - this.___lastCount === 0) { this.___emitLast(); } }, ___doFinish: function () { var state = this.___state; state.___finished = true; state.___events.emit(EVENT_FINISH, this.___getResult()); }, ___emitLast: 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.___sync) { 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.___state; if (options) { if (options.last) { this.___lastCount++; } } this.___remaining++; var documentFragment = this.___parent.___appendDocumentFragment(); var asyncOut = new AsyncVDOMBuilder(this.global, documentFragment, this); state.___events.emit("beginAsync", { out: asyncOut, parentOut: this, }); return asyncOut; }, createOut: function () { return new AsyncVDOMBuilder(this.global); }, flush: function () { var events = this.___state.___events; if (events.listenerCount(EVENT_UPDATE)) { events.emit(EVENT_UPDATE, new RenderResult(this)); } }, ___getOutput: function () { return this.___state.___tree; }, ___getResult: function () { return this.___result || (this.___result = new RenderResult(this)); }, on: function (event, callback) { var state = this.___state; if (event === EVENT_FINISH && state.___finished) { callback(this.___getResult()); } else if (event === "last") { this.onLast(callback); } else { state.___events.on(event, callback); } return this; }, once: function (event, callback) { var state = this.___state; if (event === EVENT_FINISH && state.___finished) { callback(this.___getResult()); } else if (event === "last") { this.onLast(callback); } else { state.___events.once(event, callback); } return this; }, emit: function (type, arg) { var events = this.___state.___events; 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.___state.___events; events.removeListener.apply(events, arguments); return this; }, sync: function () { this.___sync = true; }, isSync: function () { return this.___sync; }, onLast: function (callback) { var lastArray = this._last; if (lastArray === undefined) { this._last = [callback]; } else { lastArray.push(callback); } return this; }, ___getNode: function (host) { var node = this.___vnode; if (!node) { var vdomTree = this.___getOutput(); if (!host) host = this.___host; this.___vnode = node = vdomTree.___actualize(host, null); morphdom(node, vdomTree, host, this.___components); } return node; }, toString: function (host) { var docFragment = this.___getNode(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.___assignedComponentDef = componentDef; this.___assignedKey = key; this.___assignedCustomEvents = customEvents; }, }); proto.e = proto.element; proto.be = proto.beginElement; proto.ee = proto.___endElement = proto.endElement; proto.t = proto.text; proto.h = proto.w = proto.write = proto.html; module.exports = AsyncVDOMBuilder;