UNPKG

marko

Version:

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

663 lines (530 loc) • 14.5 kB
"use strict"; // eslint-disable-next-line no-constant-binary-expression var EventEmitter = require("events-light"); var SubscriptionTracker = require("listener-tracker"); var inherit = require("raptor-util/inherit"); var componentsUtil = require("@internal/components-util"); var componentLookup = componentsUtil._n_; var destroyNodeRecursive = componentsUtil._T_; var defaultCreateOut = require("../createOut"); var domInsert = require("../dom-insert"); var RenderResult = require("../RenderResult"); var morphdom = require("../vdom/morphdom"); var getComponentsContext = require("./ComponentsContext").T_; var domData = require("./dom-data"); var eventDelegation = require("./event-delegation"); var updateManager = require("./update-manager"); var componentsByDOMNode = domData._r_; var keyedElementsByComponentId = domData._q_; var CONTEXT_KEY = "__subtree_context__"; var hasOwnProperty = Object.prototype.hasOwnProperty; var slice = Array.prototype.slice; var COMPONENT_SUBSCRIBE_TO_OPTIONS; var NON_COMPONENT_SUBSCRIBE_TO_OPTIONS = { addDestroyListener: false }; var emit = EventEmitter.prototype.emit; var ELEMENT_NODE = 1; function removeListener(removeEventListenerHandle) { removeEventListenerHandle(); } function walkFragments(fragment) { var node; while (fragment) { node = fragment.firstChild; if (!node) { break; } fragment = node.fragment; } return node; } function handleCustomEventWithMethodListener( component, targetMethodName, args, extraArgs) { // Remove the "eventType" argument args.push(component); if (extraArgs) { args = extraArgs.concat(args); } var targetComponent = componentLookup[component.Y_]; var targetMethod = typeof targetMethodName === "function" ? targetMethodName : targetComponent[targetMethodName]; if (!targetMethod) { throw Error("Method not found: " + targetMethodName); } targetMethod.apply(targetComponent, args); } function resolveKeyHelper(key, index) { return index ? key + "_" + index : key; } function resolveComponentIdHelper(component, key, index) { return component.id + "-" + resolveKeyHelper(key, index); } /** * This method is used to process "update_<stateName>" handler functions. * If all of the modified state properties have a user provided update handler * then a rerender will be bypassed and, instead, the DOM will be updated * looping over and invoking the custom update handlers. * @return {boolean} Returns true if if the DOM was updated. False, otherwise. */ function processUpdateHandlers(component, stateChanges, oldState) { var handlerMethod; var handlers; for (var propName in stateChanges) { if (hasOwnProperty.call(stateChanges, propName)) { var handlerMethodName = "update_" + propName; handlerMethod = component[handlerMethodName]; if (handlerMethod) { (handlers || (handlers = [])).push([propName, handlerMethod]); } else { // This state change does not have a state handler so return false // to force a rerender return; } } } // If we got here then all of the changed state properties have // an update handler or there are no state properties that actually // changed. if (handlers) { // Otherwise, there are handlers for all of the changed properties // so apply the updates using those handlers handlers.forEach(function (handler) { var propertyName = handler[0]; handlerMethod = handler[1]; var newValue = stateChanges[propertyName]; var oldValue = oldState[propertyName]; handlerMethod.call(component, newValue, oldValue); }); component._A_(); component._v_(); } return true; } function checkInputChanged(existingComponent, oldInput, newInput) { if (oldInput != newInput) { if (oldInput == null || newInput == null) { return true; } var oldKeys = Object.keys(oldInput); var newKeys = Object.keys(newInput); var len = oldKeys.length; if (len !== newKeys.length) { return true; } for (var i = len; i--;) { var key = oldKeys[i]; if (!(key in newInput && oldInput[key] === newInput[key])) { return true; } } } return false; } var componentProto; /** * Base component type. * * NOTE: Any methods that are prefixed with an underscore should be considered private! */ function Component(id) { EventEmitter.call(this); this.id = id; this.A_ = null; this._G_ = null; this.ak_ = null; this._y_ = null; this.Z_ = null; // Used to keep track of bubbling DOM events for components rendered on the server this.X_ = null; this.Y_ = null; this.al_ = null; this.P_ = undefined; this._z_ = false; this.am_ = undefined; this.J_ = false; this._c_ = false; this.an_ = false; this.ao_ = false; this.C_ = undefined; var ssrKeyedElements = keyedElementsByComponentId[id]; if (ssrKeyedElements) { this.L_ = ssrKeyedElements; delete keyedElementsByComponentId[id]; } else { this.L_ = {}; } } Component.prototype = componentProto = { y_: true, subscribeTo: function (target) { if (!target) { throw TypeError(); } var subscriptions = this.ak_ || ( this.ak_ = new SubscriptionTracker()); var subscribeToOptions = target.y_ ? COMPONENT_SUBSCRIBE_TO_OPTIONS : NON_COMPONENT_SUBSCRIBE_TO_OPTIONS; return subscriptions.subscribeTo(target, subscribeToOptions); }, emit: function (eventType) { var customEvents = this.X_; var target; if (customEvents && (target = customEvents[eventType])) { var targetMethodName = target[0]; var isOnce = target[1]; var extraArgs = target[2]; var args = slice.call(arguments, 1); handleCustomEventWithMethodListener( this, targetMethodName, args, extraArgs ); if (isOnce) { delete customEvents[eventType]; } } return emit.apply(this, arguments); }, getElId: function (key, index) { if (!key) { return this.id; } return resolveComponentIdHelper(this, key, index); }, getEl: function (key, index) { if (key) { var resolvedKey = resolveKeyHelper(key, index); var keyedElement = this.L_["@" + resolvedKey]; if (keyedElement && keyedElement.nodeType === 12 /** FRAGMENT_NODE */) { // eslint-disable-next-line no-constant-condition return walkFragments(keyedElement); } return keyedElement; } else { return this.el; } }, getEls: function (key) { key = key + "[]"; var els = []; var i = 0; var el; while (el = this.getEl(key, i)) { els.push(el); i++; } return els; }, getComponent: function (key, index) { var rootNode = this.L_["@" + resolveKeyHelper(key, index)]; if (/\[\]$/.test(key)) { // eslint-disable-next-line no-constant-condition rootNode = rootNode && rootNode[Object.keys(rootNode)[0]]; } return rootNode && componentsByDOMNode.get(rootNode); }, getComponents: function (key) { var lookup = this.L_["@" + key + "[]"]; return lookup ? Object.keys(lookup). map(function (key) { return componentsByDOMNode.get(lookup[key]); }). filter(Boolean) : []; }, destroy: function () { if (this.J_) { return; } var root = this._G_; this._P_(); var nodes = root.nodes; nodes.forEach(function (node) { destroyNodeRecursive(node); if (eventDelegation.ap_(node) !== false) { node.parentNode.removeChild(node); } }); root.detached = true; delete componentLookup[this.id]; this.L_ = {}; }, _P_: function () { if (this.J_) { return; } this.aq_(); this.J_ = true; componentsByDOMNode.set(this._G_, undefined); this._G_ = null; // Unsubscribe from all DOM events this._x_(); var subscriptions = this.ak_; if (subscriptions) { subscriptions.removeAllListeners(); this.ak_ = null; } }, isDestroyed: function () { return this.J_; }, get state() { return this.A_; }, set state(newState) { var state = this.A_; if (!state && !newState) { return; } if (!state) { state = this.A_ = new this.Q_(this); } state.ar_(newState || {}); if (state.an_) { this.as_(); } if (!newState) { this.A_ = null; } }, setState: function (name, value) { var state = this.A_; if (!state) { state = this.A_ = new this.Q_(this); } if (typeof name == "object") { // Merge in the new state with the old state var newState = name; for (var k in newState) { if (hasOwnProperty.call(newState, k)) { state.at_(k, newState[k], true /* ensure:true */); } } } else { state.at_(name, value, true /* ensure:true */); } }, setStateDirty: function (name, value) { var state = this.A_; if (arguments.length == 1) { value = state[name]; } state.at_( name, value, true /* ensure:true */, true /* forceDirty:true */ ); }, replaceState: function (newState) { this.A_.ar_(newState); }, get input() { return this.P_; }, set input(newInput) { if (this.ao_) { this.P_ = newInput; } else { this._k_(newInput); } }, _k_: function (newInput, onInput, out) { onInput = onInput || this.onInput; var updatedInput; var oldInput = this.P_; this.P_ = undefined; this.au_ = out && out[CONTEXT_KEY] || this.au_; if (onInput) { // We need to set a flag to preview `this.input = foo` inside // onInput causing infinite recursion this.ao_ = true; updatedInput = onInput.call(this, newInput || {}, out); this.ao_ = false; } newInput = this.al_ = updatedInput || newInput; if (this.an_ = checkInputChanged(this, oldInput, newInput)) { this.as_(); } if (this.P_ === undefined) { this.P_ = newInput; if (newInput && newInput.$global) { this.am_ = newInput.$global; } } return newInput; }, forceUpdate: function () { this.an_ = true; this.as_(); }, as_: function () { if (!this._c_) { this._c_ = true; updateManager.av_(this); } }, update: function () { if (this.J_ === true || this.aw_ === false) { return; } var input = this.P_; var state = this.A_; if (this.an_ === false && state !== null && state.an_ === true) { if (processUpdateHandlers(this, state.ax_, state.ay_, state)) { state.an_ = false; } } if (this.aw_ === true) { // The UI component is still dirty after process state handlers // then we should rerender if (this.shouldUpdate(input, state) !== false) { this.az_(); } } this._v_(); }, get aw_() { return ( this.an_ === true || this.A_ !== null && this.A_.an_ === true); }, _v_: function () { this.an_ = false; this._c_ = false; this.al_ = null; var state = this.A_; if (state) { state._v_(); } }, shouldUpdate: function () { return true; }, az_: function () { // eslint-disable-next-line @typescript-eslint/no-this-alias var self = this; var renderer = self.R_; if (!renderer) { throw TypeError(); } var input = this.al_ || this.P_; updateManager.aA_(function () { self._H_(input, false).afterInsert(self.C_); }); this._v_(); }, _H_: function (input, isHydrate) { var host = this.C_; var globalData = this.am_; var rootNode = this._G_; var renderer = this.R_; var createOut = renderer.createOut || defaultCreateOut; var out = createOut(globalData); out.sync(); out.C_ = this.C_; out[CONTEXT_KEY] = this.au_; var componentsContext = getComponentsContext(out); var globalComponentsContext = componentsContext.p_; globalComponentsContext.aB_ = this; globalComponentsContext.aa_ = isHydrate; renderer(input, out); var result = new RenderResult(out); var targetNode = out.aj_().aC_; morphdom(rootNode, targetNode, host, componentsContext); return result; }, aD_: function () { var root = this._G_; root.remove(); return root; }, _x_: function () { var eventListenerHandles = this._y_; if (eventListenerHandles) { eventListenerHandles.forEach(removeListener); this._y_ = null; } }, get aE_() { var state = this.A_; return state && state.B_; }, aF_: function (customEvents, scope) { var finalCustomEvents = this.X_ = {}; this.Y_ = scope; customEvents.forEach(function (customEvent) { var eventType = customEvent[0]; var targetMethodName = customEvent[1]; var isOnce = customEvent[2]; var extraArgs = customEvent[3]; if (targetMethodName) { finalCustomEvents[eventType] = [targetMethodName, isOnce, extraArgs]; } }); }, get el() { return walkFragments(this._G_); }, get els() { // eslint-disable-next-line no-constant-condition return (this._G_ ? this._G_.nodes : []).filter( function (el) { return el.nodeType === ELEMENT_NODE; } ); }, aG_: emit, aH_(input, out) { this.onCreate && this.onCreate(input, out); this.aG_("create", input, out); }, aI_(out) { this.onRender && this.onRender(out); this.aG_("render", out); }, _A_() { this.onUpdate && this.onUpdate(); this.aG_("update"); }, _B_() { this.onMount && this.onMount(); this.aG_("mount"); }, aq_() { this.onDestroy && this.onDestroy(); this.aG_("destroy"); } }; componentProto.elId = componentProto.getElId; componentProto.aJ_ = componentProto.update; componentProto.aK_ = componentProto.destroy; // Add all of the following DOM methods to Component.prototype: // - appendTo(referenceEl) // - replace(referenceEl) // - replaceChildrenOf(referenceEl) // - insertBefore(referenceEl) // - insertAfter(referenceEl) // - prependTo(referenceEl) domInsert( componentProto, function getEl(component) { return component.aD_(); }, function afterInsert(component) { return component; } ); inherit(Component, EventEmitter); module.exports = Component;