diff --git a/src/attach-component.ts b/src/attach-component.ts
index c359498..acd3371 100644
--- a/src/attach-component.ts
+++ b/src/attach-component.ts
@@ -8,6 +8,14 @@ import { createElement } from "./create-element";
import type { VelesElement, VelesComponentObject } from "./types";
+
+/**
+ * Attach Veles component tree to a regular HTML node.
+ * Right now it will wrap the app into an additional `
` tag.
+ *
+ * It returns a function which when executed, will remove the Veles
+ * tree from DOM and remove all subscriptions.
+ */
function attachComponent({
htmlElement,
component,
@@ -19,6 +27,7 @@ function attachComponent({
// for the consumers, it greatly simplifies some things, namely, mount callbacks
// for components or supporting conditional rendering at the top level
const wrappedApp = createElement("div", { children: [component] });
+ // convert Veles tree into a tree which contains rendered Nodes
const wrappedAppTree = renderTree(wrappedApp);
const velesElementNode = getExecutedComponentVelesNode(wrappedAppTree);
htmlElement.appendChild(velesElementNode.html);
diff --git a/src/create-ref.ts b/src/create-ref.ts
index 74dfca6..1caa499 100644
--- a/src/create-ref.ts
+++ b/src/create-ref.ts
@@ -1,3 +1,8 @@
+/**
+ * Create a reference which has special treatment if passed as
+ * ref={ref} to any DOM Node. `ref.current` will contain the
+ * rendered node, even if it changes.
+ */
function createRef(initialRefValue: T | null = null): {
velesRef: true;
current: T | null;
diff --git a/src/create-state/index.ts b/src/create-state/index.ts
index 88908e8..84e8a0c 100644
--- a/src/create-state/index.ts
+++ b/src/create-state/index.ts
@@ -20,6 +20,16 @@ import type {
TrackingSelectorElement,
} from "./types";
+/**
+ * Main state factory function.
+ *
+ * This primitive is pretty much a simple observable implementation,
+ * which is tightly integrated with the UI framework for two things:
+ *
+ * - based on subscription callback, update DOM node and replace it
+ * - correctly unsbuscribe when the Node/component is unmounted
+ */
+
function createState(
initialValue: T,
subscribeCallback?: (
@@ -29,6 +39,7 @@ function createState(
let value = initialValue;
let previousValue: undefined | T = undefined;
+ // all subscription types we track
const trackers: StateTrackers = {
trackingEffects: [],
trackingSelectorElements: [],
diff --git a/src/create-state/update-useattribute-value.ts b/src/create-state/update-useattribute-value.ts
index 54f1a02..5151a56 100644
--- a/src/create-state/update-useattribute-value.ts
+++ b/src/create-state/update-useattribute-value.ts
@@ -10,12 +10,16 @@ function updateUseAttributeValue({
const { cb, htmlElement, attributeName, attributeValue } = element;
const newAttributeValue = cb ? cb(value) : value;
+ // Boolean elements require either setting an empty string as a value,
+ // or duplicate the attribute name. A lack of the attribute means
+ // the value is `false`, so we need to treat it differently.
if (typeof newAttributeValue === "boolean") {
if (newAttributeValue) {
htmlElement.setAttribute(attributeName, "");
} else {
htmlElement.removeAttribute(attributeName);
}
+ // check whether we are dealing with event handlers
} else if (attributeName.startsWith("on")) {
// if the value is the same, it is either not set
// or we received the same event handler
@@ -27,6 +31,8 @@ function updateUseAttributeValue({
const eventName =
attributeName[2].toLocaleLowerCase() + attributeName.slice(3);
if (attributeValue) {
+ // we remove the previous value, `removeEventListener` needs
+ // to have the same value as the one that was added
htmlElement.removeEventListener(eventName, attributeValue);
}
if (newAttributeValue && typeof newAttributeValue === "function") {
@@ -34,6 +40,8 @@ function updateUseAttributeValue({
}
// not the best approach, but it should work as expected
// basically, update the array value in-place
+ // we update it so that we can compare to the previous value if needed
+ // and to remove a correct event handler
element.attributeValue = newAttributeValue;
} else {
htmlElement.setAttribute(attributeName, newAttributeValue);
diff --git a/src/create-state/update-usevalue-selector-value.ts b/src/create-state/update-usevalue-selector-value.ts
index 4da8f34..a9c1ffa 100644
--- a/src/create-state/update-usevalue-selector-value.ts
+++ b/src/create-state/update-usevalue-selector-value.ts
@@ -31,10 +31,23 @@ function updateUseValueSelector({
const newSelectedValue = selector ? selector(value) : value;
if (comparator(selectedValue, newSelectedValue)) {
+ /**
+ * if there is no need for update, we push the existing element
+ * to the new array. once we merge all subscriptions, we run
+ * `unique` function which will make sure there are no double
+ * subscriptions.
+ *
+ * This is needed because using `map` can potentially create
+ * some weird side effects, since in case the node changed,
+ * some elements will be dynamically removed from the array
+ */
+
newTrackingSelectorElements.push(selectorTrackingElement);
return;
}
+ // we need to re-execute the rendering callback with the same
+ // context values as before
addPublicContext(savedContext);
const returnednewNode = cb
? cb(newSelectedValue)
@@ -46,7 +59,11 @@ function updateUseValueSelector({
? createTextElement(returnednewNode as string)
: returnednewNode;
+ // Since we render a new Node, we need to insert it into the DOM
+ // manually and immediately. So we render the full HTML tree.
const newRenderedNode = renderTree(newNode);
+ // this should remove our saved context value from the stack
+ // so that other components will be executed within their own context
popPublicContext();
newNode.executedVersion = newRenderedNode;
@@ -67,6 +84,8 @@ function updateUseValueSelector({
const parentVelesElement = node.parentVelesElement;
const parentVelesElementRendered = oldVelesElementNode.parentVelesElement;
+ // at this point we can construct the new tracking selector element
+ // the old will be removed by the unmount lifecycle hook from the node
const newTrackingSelectorElement: TrackingSelectorElement = {
selector,
selectedValue: newSelectedValue,
@@ -214,6 +233,7 @@ function updateUseValueSelector({
}
// we call unmount handlers right after we replace it
+ // this is where the old
callUnmountHandlers(node.executedVersion);
addUseValueMountHandler({
@@ -223,7 +243,7 @@ function updateUseValueSelector({
trackingSelectorElement: newTrackingSelectorElement,
});
// at this point the new Node is mounted, childComponents are updated
- // and unmount handlers for the old node are called
+ // old tracking selector element will be removed in the `unmount` handler
callMountHandlers(newRenderedNode);
// right after that, we add the callback back
diff --git a/src/create-state/update-usevalueiterator-value.ts b/src/create-state/update-usevalueiterator-value.ts
index 6795caf..f68353c 100644
--- a/src/create-state/update-usevalueiterator-value.ts
+++ b/src/create-state/update-usevalueiterator-value.ts
@@ -80,7 +80,7 @@ function updateUseValueIteratorValue({
[calculatedKey: string]: boolean;
} = {};
- elements.forEach((element, index) => {
+ elements.forEach((element, index) => {
let calculatedKey: string = "";
if (
typeof key === "string" &&
@@ -99,6 +99,25 @@ function updateUseValueIteratorValue({
return;
}
+ // first, we check if there is a node by this key
+ // if there is, we do `getValue()` and compare whether the
+ // item is the same.
+ // if it is not, we need to do `elementState.setValue()`
+ // with the new value
+ // if the value is the same, nothing to do.
+ //
+ // after that, we need to put the new position down
+ // (we'll reshuffle items at the end)
+ //
+ // if there is no node by this key, we need to:
+ // 1. create a state for it
+ // 2. create a node for it
+ // 3. mark the new index for that node
+ //
+ // at the end, we need to find elements which were rendered, but are
+ // not rendered anymore, and remove them from DOM and trigger `onUnmount`
+ // for them.
+
const existingElement = elementsByKey[calculatedKey];
if (existingElement) {
@@ -144,25 +163,6 @@ function updateUseValueIteratorValue({
node,
};
}
-
- // first, we check if there is a node by this key
- // if there is, we do `getValue()` and compare whether the
- // item is the same.
- // if it is not, we need to do `elementState.setValue()`
- // with the new value
- // if the value is the same, nothing to do.
- //
- // after that, we need to put the new position down
- // (we'll reshuffle items at the end)
- //
- // if there is no node by this key, we need to:
- // 1. create a state for it
- // 2. create a node for it
- // 3. mark the new index for that node
- //
- // at the end, we need to find elements which were rendered, but are
- // not rendered anymore, and remove them from DOM and trigger `onUnmount`
- // for them.
});
// to replace old wrapper's children to make sure they are removed correctly
diff --git a/src/hooks/lifecycle.ts b/src/hooks/lifecycle.ts
index c929746..a1e02ed 100644
--- a/src/hooks/lifecycle.ts
+++ b/src/hooks/lifecycle.ts
@@ -3,11 +3,11 @@ import { ComponentAPI } from "../types";
// lifecycle hooks
// currently, all components need to be synchronous
// so we execute them and set background context
-// since components can be nested, we need to keep the array
+// since components can be nested, we need to use the stack
const contextStack: ComponentAPI[] = [];
// all hooks need to know the current context
-// it should way more convenient this way compared to passing
-// `componentAPI` to every method
+// it should be way more convenient to use it this way
+// compared to passing `componentAPI` to every method
let currentContext: ComponentAPI | null = null;
function addContext(newContext: ComponentAPI) {