UNPKG

marko

Version:

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

662 lines (529 loc) • 14.4 kB
"use strict"; /* jshint newcap:false */ 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._k_; var destroyNodeRecursive = componentsUtil._Q_; var defaultCreateOut = require("../createOut"); var domInsert = require("../dom-insert"); var RenderResult = require("../RenderResult"); var morphdom = require("../vdom/morphdom"); var getComponentsContext = require("./ComponentsContext").S_; var domData = require("./dom-data"); var eventDelegation = require("./event-delegation"); var updateManager = require("./update-manager"); var componentsByDOMNode = domData._o_; var keyedElementsByComponentId = domData._n_; 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.W_]; 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._x_(); component._s_(); } 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.z_ = null; this._D_ = null; this.ah_ = null; this._v_ = null; this.X_ = null; // Used to keep track of bubbling DOM events for components rendered on the server this.V_ = null; this.W_ = null; this.ai_ = null; this.O_ = undefined; this._w_ = false; this.aj_ = undefined; this.I_ = false; this._a_ = false; this.ak_ = false; this.al_ = false; this.B_ = undefined; var ssrKeyedElements = keyedElementsByComponentId[id]; if (ssrKeyedElements) { this.K_ = ssrKeyedElements; delete keyedElementsByComponentId[id]; } else { this.K_ = {}; } } Component.prototype = componentProto = { x_: true, subscribeTo: function (target) { if (!target) { throw TypeError(); } var subscriptions = this.ah_ || ( this.ah_ = new SubscriptionTracker()); var subscribeToOptions = target.x_ ? COMPONENT_SUBSCRIBE_TO_OPTIONS : NON_COMPONENT_SUBSCRIBE_TO_OPTIONS; return subscriptions.subscribeTo(target, subscribeToOptions); }, emit: function (eventType) { var customEvents = this.V_; 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.K_["@" + 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.K_["@" + 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.K_["@" + key + "[]"]; return lookup ? Object.keys(lookup). map(function (key) { return componentsByDOMNode.get(lookup[key]); }). filter(Boolean) : []; }, destroy: function () { if (this.I_) { return; } var root = this._D_; this._M_(); var nodes = root.nodes; nodes.forEach(function (node) { destroyNodeRecursive(node); if (eventDelegation.am_(node) !== false) { node.parentNode.removeChild(node); } }); root.detached = true; delete componentLookup[this.id]; this.K_ = {}; }, _M_: function () { if (this.I_) { return; } this.an_(); this.I_ = true; componentsByDOMNode.set(this._D_, undefined); this._D_ = null; // Unsubscribe from all DOM events this._u_(); var subscriptions = this.ah_; if (subscriptions) { subscriptions.removeAllListeners(); this.ah_ = null; } }, isDestroyed: function () { return this.I_; }, get state() { return this.z_; }, set state(newState) { var state = this.z_; if (!state && !newState) { return; } if (!state) { state = this.z_ = new this.P_(this); } state.ao_(newState || {}); if (state.ak_) { this.ap_(); } if (!newState) { this.z_ = null; } }, setState: function (name, value) { var state = this.z_; if (!state) { state = this.z_ = new this.P_(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.aq_(k, newState[k], true /* ensure:true */); } } } else { state.aq_(name, value, true /* ensure:true */); } }, setStateDirty: function (name, value) { var state = this.z_; if (arguments.length == 1) { value = state[name]; } state.aq_( name, value, true /* ensure:true */, true /* forceDirty:true */ ); }, replaceState: function (newState) { this.z_.ao_(newState); }, get input() { return this.O_; }, set input(newInput) { if (this.al_) { this.O_ = newInput; } else { this._h_(newInput); } }, _h_: function (newInput, onInput, out) { onInput = onInput || this.onInput; var updatedInput; var oldInput = this.O_; this.O_ = undefined; this.ar_ = out && out[CONTEXT_KEY] || this.ar_; if (onInput) { // We need to set a flag to preview `this.input = foo` inside // onInput causing infinite recursion this.al_ = true; updatedInput = onInput.call(this, newInput || {}, out); this.al_ = false; } newInput = this.ai_ = updatedInput || newInput; if (this.ak_ = checkInputChanged(this, oldInput, newInput)) { this.ap_(); } if (this.O_ === undefined) { this.O_ = newInput; if (newInput && newInput.$global) { this.aj_ = newInput.$global; } } return newInput; }, forceUpdate: function () { this.ak_ = true; this.ap_(); }, ap_: function () { if (!this._a_) { this._a_ = true; updateManager.as_(this); } }, update: function () { if (this.I_ === true || this.at_ === false) { return; } var input = this.O_; var state = this.z_; if (this.ak_ === false && state !== null && state.ak_ === true) { if (processUpdateHandlers(this, state.au_, state.av_, state)) { state.ak_ = false; } } if (this.at_ === true) { // The UI component is still dirty after process state handlers // then we should rerender if (this.shouldUpdate(input, state) !== false) { this.aw_(); } } this._s_(); }, get at_() { return ( this.ak_ === true || this.z_ !== null && this.z_.ak_ === true); }, _s_: function () { this.ak_ = false; this._a_ = false; this.ai_ = null; var state = this.z_; if (state) { state._s_(); } }, shouldUpdate: function () { return true; }, aw_: function () { var self = this; var renderer = self.Q_; if (!renderer) { throw TypeError(); } var input = this.ai_ || this.O_; updateManager.ax_(function () { self._E_(input, false).afterInsert(self.B_); }); this._s_(); }, _E_: function (input, isHydrate) { var host = this.B_; var globalData = this.aj_; var rootNode = this._D_; var renderer = this.Q_; var createOut = renderer.createOut || defaultCreateOut; var out = createOut(globalData); out.sync(); out.B_ = this.B_; out[CONTEXT_KEY] = this.ar_; var componentsContext = getComponentsContext(out); var globalComponentsContext = componentsContext.p_; globalComponentsContext.ay_ = this; globalComponentsContext._Y_ = isHydrate; renderer(input, out); var result = new RenderResult(out); var targetNode = out.ag_().az_; morphdom(rootNode, targetNode, host, componentsContext); return result; }, aA_: function () { var root = this._D_; root.remove(); return root; }, _u_: function () { var eventListenerHandles = this._v_; if (eventListenerHandles) { eventListenerHandles.forEach(removeListener); this._v_ = null; } }, get aB_() { var state = this.z_; return state && state.A_; }, aC_: function (customEvents, scope) { var finalCustomEvents = this.V_ = {}; this.W_ = 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._D_); }, get els() { // eslint-disable-next-line no-constant-condition return (this._D_ ? this._D_.nodes : []).filter( function (el) { return el.nodeType === ELEMENT_NODE; } ); }, aD_: emit, aE_(input, out) { this.onCreate && this.onCreate(input, out); this.aD_("create", input, out); }, aF_(out) { this.onRender && this.onRender(out); this.aD_("render", out); }, _x_() { this.onUpdate && this.onUpdate(); this.aD_("update"); }, _y_() { this.onMount && this.onMount(); this.aD_("mount"); }, an_() { this.onDestroy && this.onDestroy(); this.aD_("destroy"); } }; componentProto.elId = componentProto.getElId; componentProto.aG_ = componentProto.update; componentProto.aH_ = 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.aA_(); }, function afterInsert(component) { return component; } ); inherit(Component, EventEmitter); module.exports = Component;