marko
Version:
UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.
664 lines (547 loc) • 16.6 kB
JavaScript
"use strict";
// eslint-disable-next-line no-constant-binary-expression
var complain = "MARKO_DEBUG" && require("complain");
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.___componentLookup;
var destroyNodeRecursive = componentsUtil.___destroyNodeRecursive;
var defaultCreateOut = require("../createOut");
var domInsert = require("../dom-insert");
var RenderResult = require("../RenderResult");
var morphdom = require("../vdom/morphdom");
var getComponentsContext =
require("./ComponentsContext").___getComponentsContext;
var domData = require("./dom-data");
var eventDelegation = require("./event-delegation");
var updateManager = require("./update-manager");
var componentsByDOMNode = domData.___componentByDOMNode;
var keyedElementsByComponentId = domData.___ssrKeyedElementsByComponentId;
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.___scope];
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.___emitUpdate();
component.___reset();
}
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.___state = null;
this.___rootNode = null;
this.___subscriptions = null;
this.___domEventListenerHandles = null;
this.___bubblingDomEvents = null; // Used to keep track of bubbling DOM events for components rendered on the server
this.___customEvents = null;
this.___scope = null;
this.___renderInput = null;
this.___input = undefined;
this.___mounted = false;
this.___global = undefined;
this.___destroyed = false;
this.___updateQueued = false;
this.___dirty = false;
this.___settingInput = false;
this.___host = undefined;
var ssrKeyedElements = keyedElementsByComponentId[id];
if (ssrKeyedElements) {
this.___keyedElements = ssrKeyedElements;
delete keyedElementsByComponentId[id];
} else {
this.___keyedElements = {};
}
}
Component.prototype = componentProto = {
___isComponent: true,
subscribeTo: function (target) {
if (!target) {
throw TypeError();
}
var subscriptions =
this.___subscriptions ||
(this.___subscriptions = new SubscriptionTracker());
var subscribeToOptions = target.___isComponent
? COMPONENT_SUBSCRIBE_TO_OPTIONS
: NON_COMPONENT_SUBSCRIBE_TO_OPTIONS;
return subscriptions.subscribeTo(target, subscribeToOptions);
},
emit: function (eventType) {
var customEvents = this.___customEvents;
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.___keyedElements["@" + resolvedKey];
if (keyedElement && keyedElement.nodeType === 12 /** FRAGMENT_NODE */) {
// eslint-disable-next-line no-constant-condition
if ("MARKO_DEBUG") {
complain(
"Accessing the elements of a child component using 'component.getEl' is deprecated.",
);
}
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.___keyedElements["@" + resolveKeyHelper(key, index)];
if (/\[\]$/.test(key)) {
// eslint-disable-next-line no-constant-condition
if ("MARKO_DEBUG") {
complain(
"A repeated key[] was passed to getComponent. Use a non-repeating key if there is only one of these components.",
);
}
rootNode = rootNode && rootNode[Object.keys(rootNode)[0]];
}
return rootNode && componentsByDOMNode.get(rootNode);
},
getComponents: function (key) {
var lookup = this.___keyedElements["@" + key + "[]"];
return lookup
? Object.keys(lookup)
.map(function (key) {
return componentsByDOMNode.get(lookup[key]);
})
.filter(Boolean)
: [];
},
destroy: function () {
if (this.___destroyed) {
return;
}
var root = this.___rootNode;
this.___destroyShallow();
var nodes = root.nodes;
nodes.forEach(function (node) {
destroyNodeRecursive(node);
if (eventDelegation.___handleNodeDetach(node) !== false) {
node.parentNode.removeChild(node);
}
});
root.detached = true;
delete componentLookup[this.id];
this.___keyedElements = {};
},
___destroyShallow: function () {
if (this.___destroyed) {
return;
}
this.___emitDestroy();
this.___destroyed = true;
componentsByDOMNode.set(this.___rootNode, undefined);
this.___rootNode = null;
// Unsubscribe from all DOM events
this.___removeDOMEventListeners();
var subscriptions = this.___subscriptions;
if (subscriptions) {
subscriptions.removeAllListeners();
this.___subscriptions = null;
}
},
isDestroyed: function () {
return this.___destroyed;
},
get state() {
return this.___state;
},
set state(newState) {
var state = this.___state;
if (!state && !newState) {
return;
}
if (!state) {
state = this.___state = new this.___State(this);
}
state.___replace(newState || {});
if (state.___dirty) {
this.___queueUpdate();
}
if (!newState) {
this.___state = null;
}
},
setState: function (name, value) {
var state = this.___state;
if (!state) {
state = this.___state = new this.___State(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.___set(k, newState[k], true /* ensure:true */);
}
}
} else {
state.___set(name, value, true /* ensure:true */);
}
},
setStateDirty: function (name, value) {
var state = this.___state;
if (arguments.length == 1) {
value = state[name];
}
state.___set(
name,
value,
true /* ensure:true */,
true /* forceDirty:true */,
);
},
replaceState: function (newState) {
this.___state.___replace(newState);
},
get input() {
return this.___input;
},
set input(newInput) {
if (this.___settingInput) {
this.___input = newInput;
} else {
this.___setInput(newInput);
}
},
___setInput: function (newInput, onInput, out) {
onInput = onInput || this.onInput;
var updatedInput;
var oldInput = this.___input;
this.___input = undefined;
this.___context = (out && out[CONTEXT_KEY]) || this.___context;
if (onInput) {
// We need to set a flag to preview `this.input = foo` inside
// onInput causing infinite recursion
this.___settingInput = true;
updatedInput = onInput.call(this, newInput || {}, out);
this.___settingInput = false;
}
newInput = this.___renderInput = updatedInput || newInput;
if ((this.___dirty = checkInputChanged(this, oldInput, newInput))) {
this.___queueUpdate();
}
if (this.___input === undefined) {
this.___input = newInput;
if (newInput && newInput.$global) {
this.___global = newInput.$global;
}
}
return newInput;
},
forceUpdate: function () {
this.___dirty = true;
this.___queueUpdate();
},
___queueUpdate: function () {
if (!this.___updateQueued) {
this.___updateQueued = true;
updateManager.___queueComponentUpdate(this);
}
},
update: function () {
if (this.___destroyed === true || this.___isDirty === false) {
return;
}
var input = this.___input;
var state = this.___state;
if (this.___dirty === false && state !== null && state.___dirty === true) {
if (processUpdateHandlers(this, state.___changes, state.___old, state)) {
state.___dirty = false;
}
}
if (this.___isDirty === true) {
// The UI component is still dirty after process state handlers
// then we should rerender
if (this.shouldUpdate(input, state) !== false) {
this.___scheduleRerender();
}
}
this.___reset();
},
get ___isDirty() {
return (
this.___dirty === true ||
(this.___state !== null && this.___state.___dirty === true)
);
},
___reset: function () {
this.___dirty = false;
this.___updateQueued = false;
this.___renderInput = null;
var state = this.___state;
if (state) {
state.___reset();
}
},
shouldUpdate: function () {
return true;
},
___scheduleRerender: function () {
// eslint-disable-next-line @typescript-eslint/no-this-alias
var self = this;
var renderer = self.___renderer;
if (!renderer) {
throw TypeError();
}
var input = this.___renderInput || this.___input;
updateManager.___batchUpdate(function () {
self.___rerender(input, false).afterInsert(self.___host);
});
this.___reset();
},
___rerender: function (input, isHydrate) {
var host = this.___host;
var globalData = this.___global;
var rootNode = this.___rootNode;
var renderer = this.___renderer;
var createOut = renderer.createOut || defaultCreateOut;
var out = createOut(globalData);
out.sync();
out.___host = this.___host;
out[CONTEXT_KEY] = this.___context;
var componentsContext = getComponentsContext(out);
var globalComponentsContext = componentsContext.___globalContext;
globalComponentsContext.___rerenderComponent = this;
globalComponentsContext.___isHydrate = isHydrate;
renderer(input, out);
var result = new RenderResult(out);
var targetNode = out.___getOutput().___firstChild;
morphdom(rootNode, targetNode, host, componentsContext);
return result;
},
___detach: function () {
var root = this.___rootNode;
root.remove();
return root;
},
___removeDOMEventListeners: function () {
var eventListenerHandles = this.___domEventListenerHandles;
if (eventListenerHandles) {
eventListenerHandles.forEach(removeListener);
this.___domEventListenerHandles = null;
}
},
get ___rawState() {
var state = this.___state;
return state && state.___raw;
},
___setCustomEvents: function (customEvents, scope) {
var finalCustomEvents = (this.___customEvents = {});
this.___scope = 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.___rootNode);
},
get els() {
// eslint-disable-next-line no-constant-condition
if ("MARKO_DEBUG") {
complain(
'The "this.els" attribute is deprecated. Please use "this.getEls(key)" instead.',
);
}
return (this.___rootNode ? this.___rootNode.nodes : []).filter(
function (el) {
return el.nodeType === ELEMENT_NODE;
},
);
},
___emit: emit,
___emitCreate(input, out) {
this.onCreate && this.onCreate(input, out);
this.___emit("create", input, out);
},
___emitRender(out) {
this.onRender && this.onRender(out);
this.___emit("render", out);
},
___emitUpdate() {
this.onUpdate && this.onUpdate();
this.___emit("update");
},
___emitMount() {
this.onMount && this.onMount();
this.___emit("mount");
},
___emitDestroy() {
this.onDestroy && this.onDestroy();
this.___emit("destroy");
},
};
componentProto.elId = componentProto.getElId;
componentProto.___update = componentProto.update;
componentProto.___destroy = 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.___detach();
},
function afterInsert(component) {
return component;
},
);
inherit(Component, EventEmitter);
module.exports = Component;