From ae4fbbc2ebb01ad221702faff4250c4b794dbc02 Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Sat, 15 Jun 2024 00:49:35 -0700 Subject: [PATCH] add code comments --- src/attach-component.ts | 9 +++++ src/create-ref.ts | 5 +++ src/create-state/index.ts | 11 +++++ src/create-state/update-useattribute-value.ts | 8 ++++ .../update-usevalue-selector-value.ts | 22 +++++++++- .../update-usevalueiterator-value.ts | 40 +++++++++---------- src/hooks/lifecycle.ts | 6 +-- 7 files changed, 77 insertions(+), 24 deletions(-) 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) {