Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

forgo

Package Overview
Dependencies
Maintainers
1
Versions
140
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

forgo - npm Package Compare versions

Comparing version 2.2.3 to 3.0.0

CHANGELOG.md

28

dist/index.d.ts

@@ -65,2 +65,6 @@ export declare type ForgoRef<T> = {

export declare type ForgoPrimitiveNode = ForgoNonEmptyPrimitiveNode | null | undefined;
/**
* Anything renderable by Forgo, whether from an external source (e.g.,
* component.render() output), or internally (e.g., DOM nodes)
*/
export declare type ForgoNode = ForgoPrimitiveNode | ForgoElement<any> | ForgoFragment;

@@ -104,11 +108,31 @@ export declare type NodeAttachedComponentState<TProps> = {

};
/**
* Nodes will be created as detached DOM nodes, and will not be attached to the parent
*/
export declare type DetachedNodeInsertionOptions = {
type: "detached";
};
/**
* Instructs the renderer to search for an existing node to modify or replace,
* before creating a new node.
*/
export declare type SearchableNodeInsertionOptions = {
type: "search";
/**
* The element that holds the previously-rendered version of this component
*/
parentElement: Element;
/**
* Where under the parent's children to find the start of this component
*/
currentNodeIndex: number;
/**
* How many elements after currentNodeIndex belong to the element we're
* searching
*/
length: number;
};
/**
* Decides how the called function attaches nodes to the supplied parent
*/
export declare type NodeInsertionOptions = DetachedNodeInsertionOptions | SearchableNodeInsertionOptions;

@@ -141,2 +165,6 @@ export declare type UnloadableChildNode = {

export declare const h: typeof createElement;
/**
* Creates everything needed to run forgo, wrapped in a closure holding e.g.,
* JSDOM-specific environment overrides used in tests
*/
export declare function createForgoInstance(customEnv: any): {

@@ -143,0 +171,0 @@ mount: (forgoNode: ForgoNode, container: Element | string | null) => RenderResult;

321

dist/index.js

@@ -16,4 +16,7 @@ // Since we'll set any attribute the user passes us, we need to be sure not to

/*
The element types we care about.
As defined by the standards.
These come from the browser's Node interface, which defines an enum of node
types. We'd like to just reference Node.<whatever>, but JSDOM makes us jump
through hoops to do that because it hates adding new globals. Getting around
that is more complex, and more bytes on the wire, than just hardcoding the
constants we care about.
*/

@@ -52,2 +55,6 @@ const ELEMENT_NODE_TYPE = 1;

}
/**
* Creates everything needed to run forgo, wrapped in a closure holding e.g.,
* JSDOM-specific environment overrides used in tests
*/
export function createForgoInstance(customEnv) {

@@ -60,9 +67,13 @@ var _a;

};
/*
This is the main render function. forgoNode is the node to render.
insertionOptions specify which nodes need to be replaced by the new node(s),
or whether the new node should be created detached from the DOM (without replacement).
pendingAttachStates is the list of Component State objects which will be attached to the element.
*/
/**
* This is the main render function.
* @param forgoNode The node to render. Can be any value renderable by Forgo,
* not just DOM nodes.
* @param insertionOptions Which nodes need to be replaced by the new
* node(s), or whether the new node should be created detached from the DOM
* (without replacement).
* @param pendingAttachStates The list of Component State objects which will
* be attached to the element.
*/
function internalRender(forgoNode, insertionOptions, pendingAttachStates, mountOnPreExistingDOM) {

@@ -94,3 +105,3 @@ // Array of Nodes

*/
function renderNothing(forgoNode, insertionOptions, pendingAttachStates, mountOnPreExistingDOM) {
function renderNothing(_forgoNode, _insertionOptions, _pendingAttachStates, _mountOnPreExistingDOM) {
return { nodes: [] };

@@ -110,24 +121,18 @@ }

*/
function renderText(forgoNode, insertionOptions, pendingAttachStates, mountOnPreExistingDOM) {
function renderText(forgoNode, insertionOptions, pendingAttachStates,
// TODO: Any reason to keep this parameter?
_mountOnPreExistingDOM) {
var _a;
// We need to create a detached node.
if (insertionOptions.type === "detached") {
// Text nodes will always be recreated.
const textNode = env.document.createTextNode(stringOfPrimitiveNode(forgoNode));
syncStateAndProps(forgoNode, textNode, true, pendingAttachStates, undefined);
return { nodes: [textNode] };
}
// Text nodes will always be recreated.
const textNode = env.document.createTextNode(stringOfPrimitiveNode(forgoNode));
let oldComponentState = undefined;
// We have to find a node to replace.
else {
// Text nodes will always be recreated.
const textNode = env.document.createTextNode(stringOfPrimitiveNode(forgoNode));
if (insertionOptions.type === "search") {
const childNodes = insertionOptions.parentElement.childNodes;
// If we're searching in a list, we replace if the current node is a text node.
const childNodes = insertionOptions.parentElement.childNodes;
if (insertionOptions.length) {
let targetNode = childNodes[insertionOptions.currentNodeIndex];
const targetNode = childNodes[insertionOptions.currentNodeIndex];
if (targetNode.nodeType === TEXT_NODE_TYPE) {
targetNode.replaceWith(textNode);
const oldComponentState = (_a = getForgoState(targetNode)) === null || _a === void 0 ? void 0 : _a.components;
syncStateAndProps(forgoNode, textNode, true, pendingAttachStates, oldComponentState);
return { nodes: [textNode] };
oldComponentState = (_a = getForgoState(targetNode)) === null || _a === void 0 ? void 0 : _a.components;
}

@@ -137,24 +142,20 @@ else {

insertionOptions.parentElement.insertBefore(textNode, nextNode);
syncStateAndProps(forgoNode, textNode, true, pendingAttachStates, undefined);
return { nodes: [textNode] };
}
}
// There are no target nodes available.
else if (childNodes.length === 0 ||
insertionOptions.currentNodeIndex === 0) {
insertionOptions.parentElement.prepend(textNode);
}
else {
const childNodes = insertionOptions.parentElement.childNodes;
if (childNodes.length === 0 ||
insertionOptions.currentNodeIndex === 0) {
insertionOptions.parentElement.prepend(textNode);
}
else {
const nextNode = childNodes[insertionOptions.currentNodeIndex];
insertionOptions.parentElement.insertBefore(textNode, nextNode);
}
syncStateAndProps(forgoNode, textNode, true, pendingAttachStates, undefined);
return { nodes: [textNode] };
const nextNode = childNodes[insertionOptions.currentNodeIndex];
insertionOptions.parentElement.insertBefore(textNode, nextNode);
}
}
syncStateAndProps(forgoNode, textNode, true, pendingAttachStates, oldComponentState);
return { nodes: [textNode] };
}
/*
Render a DOM element.
Render a DOM element. Will find + update an existing DOM element (if
appropriate), or insert a new element.

@@ -183,11 +184,8 @@ Such as in the render function below:

}
else {
return addElement(insertionOptions.parentElement, childNodes[insertionOptions.currentNodeIndex]);
}
}
else {
return addElement(insertionOptions.parentElement, childNodes[insertionOptions.currentNodeIndex]);
}
return addElement(insertionOptions.parentElement, childNodes[insertionOptions.currentNodeIndex]);
}
function renderChildNodes(parentElement) {
// If the user gave us exact HTML to stuff into this parent, we can
// skip/ignore the usual rendering logic
if (forgoElement.props.dangerouslySetInnerHTML) {

@@ -198,29 +196,65 @@ parentElement.innerHTML =

else {
const forgoChildrenObj = forgoElement.props.children;
// Children will not be an array if single item.
const forgoChildren = flatten((Array.isArray(forgoChildrenObj)
? forgoChildrenObj
: [forgoChildrenObj]).filter((x) => x !== undefined && x !== null));
let currentChildNodeIndex = 0;
// Coerce children to always be an array, for simplicity
const forgoChildren = flatten([forgoElement.props.children]).filter(
// Children may or may not be specified
(x) => x !== undefined && x !== null);
// Make sure that if the user prepends non-Forgo DOM children under this
// parent that we start with the correct offset, otherwise we'll do DOM
// transformations that don't make any sense for the given input.
const firstForgoChildIndex = Array.from(parentElement.childNodes).findIndex((child) => getForgoState(child));
// Each node we render will push any leftover children further down the
// parent's list of children. After rendering everything, we can clean
// up anything extra. We'll know what's extra because all nodes we want
// to preserve come before this index.
let lastRenderedNodeIndex = firstForgoChildIndex === -1 ? 0 : firstForgoChildIndex;
for (const forgoChild of forgoChildren) {
const { nodes } = internalRender(forgoChild, {
const { nodes: nodesAfterRender } = internalRender(forgoChild, {
type: "search",
parentElement,
currentNodeIndex: currentChildNodeIndex,
length: parentElement.childNodes.length - currentChildNodeIndex,
currentNodeIndex: lastRenderedNodeIndex,
length: parentElement.childNodes.length - lastRenderedNodeIndex,
}, [], mountOnPreExistingDOM);
currentChildNodeIndex += nodes.length;
// Continue down the children list to wherever's right after the stuff
// we just added. Because users are allowed to add arbitrary stuff to
// the DOM manually, we can't just jump by the count of rendered
// elements, since that's the count of *managed* elements, which might
// be interspersed with unmanaged elements that we also need to skip
// past.
if (nodesAfterRender.length) {
while (parentElement.childNodes[lastRenderedNodeIndex] !==
nodesAfterRender[nodesAfterRender.length - 1]) {
lastRenderedNodeIndex += 1;
}
// Move the counter *past* the last node we inserted. E.g., if we just
// inserted our first node, we need to increment from 0 -> 1, where
// we'll start searching for the next thing we insert
lastRenderedNodeIndex += 1;
// If we're updating an existing DOM element, it's possible that the
// user manually added some DOM nodes somewhere in the middle of our
// managed nodes. If that happened, we need to scan forward until we
// pass them and find the next managed node, which we'll use as the
// starting point for whatever we render next. We still need the +1
// above to make sure we always progress the index, in case this is
// our first render pass and there's nothing to scan forward to.
while (lastRenderedNodeIndex < parentElement.childNodes.length) {
if (getForgoState(parentElement.childNodes[lastRenderedNodeIndex])) {
break;
}
lastRenderedNodeIndex += 1;
}
}
}
// Get rid the the remaining nodes.
const nodesToRemove = sliceNodes(parentElement.childNodes, currentChildNodeIndex, parentElement.childNodes.length);
if (nodesToRemove.length) {
markNodesForUnloading(nodesToRemove);
}
// Remove all nodes that don't correspond to the rendered output of a
// live component
markNodesForUnloading(parentElement.childNodes, lastRenderedNodeIndex, parentElement.childNodes.length);
}
}
/**
* If we're updating an element that was rendered in a previous render,
* reuse the same DOM element. Just sync its children and attributes.
*/
function renderExistingElement(insertAt, childNodes, insertionOptions) {
var _a;
// Get rid of unwanted nodes.
const nodesToRemove = sliceNodes(childNodes, insertionOptions.currentNodeIndex, insertAt);
markNodesForUnloading(nodesToRemove);
markNodesForUnloading(childNodes, insertionOptions.currentNodeIndex, insertAt);
const targetElement = childNodes[insertionOptions.currentNodeIndex];

@@ -254,30 +288,17 @@ const oldComponentState = (_a = getForgoState(targetElement)) === null || _a === void 0 ? void 0 : _a.components;

const componentIndex = pendingAttachStates.length;
if (
// We need to create a detached node.
if (insertionOptions.type === "detached") {
return addComponent();
}
// We have to find a node to replace.
else {
if (insertionOptions.length) {
if (mountOnPreExistingDOM) {
return addComponent();
}
else {
const childNodes = insertionOptions.parentElement.childNodes;
const searchResult = findReplacementCandidateForComponent(forgoElement, insertionOptions.parentElement, insertionOptions.currentNodeIndex, insertionOptions.length, pendingAttachStates.length);
if (searchResult.found) {
return renderExistingComponent(searchResult.index, childNodes, insertionOptions);
}
// No matching node found.
else {
return addComponent();
}
}
insertionOptions.type !== "detached" &&
// We have to find a node to replace.
insertionOptions.length &&
!mountOnPreExistingDOM) {
const childNodes = insertionOptions.parentElement.childNodes;
const searchResult = findReplacementCandidateForComponent(forgoElement, insertionOptions.parentElement, insertionOptions.currentNodeIndex, insertionOptions.length, pendingAttachStates.length);
if (searchResult.found) {
return renderExistingComponent(searchResult.index, childNodes, insertionOptions);
}
// No nodes in target node list.
// Nothing to unload.
else {
return addComponent();
}
}
// No nodes in target node list, or no matching node found.
// Nothing to unload.
return addComponent();
function renderExistingComponent(insertAt, childNodes, insertionOptions) {

@@ -288,6 +309,5 @@ const targetNode = childNodes[insertAt];

// Get rid of unwanted nodes.
const nodesToRemove = sliceNodes(childNodes, insertionOptions.currentNodeIndex, insertAt);
markNodesForUnloading(nodesToRemove);
markNodesForUnloading(childNodes, insertionOptions.currentNodeIndex, insertAt);
if (!componentState.component.shouldUpdate ||
componentState.component.shouldUpdate(forgoElement.props, componentState.props)) {
!componentState.component.shouldUpdate(forgoElement.props, componentState.props)) {
// Since we have compatible state already stored,

@@ -397,4 +417,3 @@ // we'll push the savedComponentState into pending states for later attachment.

const newIndex = insertionOptions.currentNodeIndex + renderResult.nodes.length;
const nodesToRemove = sliceNodes(insertionOptions.parentElement.childNodes, newIndex, newIndex + componentState.nodes.length - numNodesReused);
markNodesForUnloading(nodesToRemove);
markNodesForUnloading(insertionOptions.parentElement.childNodes, newIndex, newIndex + componentState.nodes.length - numNodesReused);
// In case we rendered an array, set the node to the first node.

@@ -452,13 +471,27 @@ // We do this because args.element.node would be set to the last node otherwise.

}
/*
This doesn't unmount components attached to these nodes,
but moves the node itself from the DOM to parentNode.__forgo_deletedNodes.
We sort of "mark" it for deletion, but it may be resurrected when a keyed,
off-order forgo node matches a deleted node.
*/
function markNodesForUnloading(nodes) {
if (nodes.length) {
const parentElement = nodes[0].parentElement;
/**
* This doesn't unmount components attached to these nodes, but moves the node
* itself from the DOM to parentNode.__forgo_deletedNodes. We sort of "mark"
* it for deletion, but it may be resurrected if it's matched by a keyed forgo
* node that has been reordered.
*
* Nodes in between `from` and `to` (not inclusive of `to`) will be marked for
* unloading. Use `unloadMarkedNodes()` to actually unload the nodes once
* we're sure we don't need to resurrect them.
*
* We don't want to remove DOM nodes that aren't owned by Forgo. I.e., if the
* user grabs a reference to a DOM element and manually adds children under
* it, we don't want to remove those children. That'll mess up e.g., charting
* libraries.
*/
function markNodesForUnloading(nodes, from, to) {
const nodesToRemove = sliceNodes(nodes, from, to);
if (nodesToRemove.length) {
const parentElement = nodesToRemove[0].parentElement;
const deletedNodes = getDeletedNodes(parentElement);
for (const node of nodes) {
for (const node of nodesToRemove) {
// If the consuming application has manually mucked with the DOM don't
// remove things it added
if (!getForgoState(node))
continue;
node.remove();

@@ -565,8 +598,8 @@ deletedNodes.push({ node });

}
/*
When we try to find replacement candidates for DOM nodes,
we try to:
a) match by the key
b) match by the tagname
*/
/**
* When we try to find replacement candidates for DOM nodes,
* we try to:
* a) match by the key
* b) match by the tagname
*/
function findReplacementCandidateForElement(forgoElement, parentElement, searchFrom, length) {

@@ -578,6 +611,10 @@ const nodes = parentElement.childNodes;

const stateOnNode = getForgoState(node);
if (forgoElement.key) {
if ((stateOnNode === null || stateOnNode === void 0 ? void 0 : stateOnNode.key) === forgoElement.key) {
return { found: true, index: i };
}
// If the user stuffs random elements into the DOM manually, we don't
// want to treat them as replacement candidates - they should be left
// alone.
if (!stateOnNode)
continue;
if (forgoElement.key !== undefined &&
(stateOnNode === null || stateOnNode === void 0 ? void 0 : stateOnNode.key) === forgoElement.key) {
return { found: true, index: i };
}

@@ -588,3 +625,3 @@ else {

if (node.tagName.toLowerCase() === forgoElement.type &&
(!stateOnNode || !stateOnNode.key)) {
!(stateOnNode === null || stateOnNode === void 0 ? void 0 : stateOnNode.key)) {
return { found: true, index: i };

@@ -596,3 +633,3 @@ }

// Let's check deleted nodes as well.
if (forgoElement.key) {
if (forgoElement.key !== undefined) {
const deletedNodes = getDeletedNodes(parentElement);

@@ -619,8 +656,8 @@ for (let i = 0; i < deletedNodes.length; i++) {

}
/*
When we try to find replacement candidates for Components,
we try to:
a) match by the key
b) match by the component constructor
*/
/**
* When we try to find replacement candidates for Components,
* we try to:
* a) match by the key
* b) match by the component constructor
*/
function findReplacementCandidateForComponent(forgoElement, parentElement, searchFrom, length, componentIndex) {

@@ -632,3 +669,3 @@ const nodes = parentElement.childNodes;

if (stateOnNode && stateOnNode.components.length > componentIndex) {
if (forgoElement.key) {
if (forgoElement.key !== undefined) {
if (stateOnNode.components[componentIndex].ctor === forgoElement.type &&

@@ -650,3 +687,4 @@ stateOnNode.components[componentIndex].key === forgoElement.key) {

if (stateOnNode && stateOnNode.components.length > componentIndex) {
if (stateOnNode.components[componentIndex].ctor === forgoElement.type && stateOnNode.components[componentIndex].key === forgoElement.key) {
if (stateOnNode.components[componentIndex].ctor === forgoElement.type &&
stateOnNode.components[componentIndex].key === forgoElement.key) {
return true;

@@ -658,3 +696,3 @@ }

// Let's check deleted nodes as well.
if (forgoElement.key) {
if (forgoElement.key !== undefined) {
const deletedNodes = getDeletedNodes(parentElement);

@@ -693,6 +731,6 @@ for (let i = 0; i < deletedNodes.length; i++) {

}
/*
Attach props from the forgoElement on to the DOM node.
We also need to attach states from pendingAttachStates
*/
/**
* Attach props from the forgoElement onto the DOM node. We also need to attach
* states from pendingAttachStates
*/
function attachProps(forgoNode, node, isNewNode, pendingAttachStates) {

@@ -869,3 +907,3 @@ var _a;

const originalComponentState = state.components[element.componentIndex];
const effectiveProps = props !== undefined ? props : originalComponentState.props;
const effectiveProps = props !== null && props !== void 0 ? props : originalComponentState.props;
if (!originalComponentState.component.shouldUpdate ||

@@ -1103,18 +1141,17 @@ originalComponentState.component.shouldUpdate(effectiveProps, originalComponentState.props)) {

}
/* parentElements.childNodes is not an array. A slice() for it. */
/**
* node.childNodes is some funky data structure that's not really not an array,
* so we can't just slice it like normal
*/
function sliceNodes(nodes, from, to) {
const result = [];
for (let i = from; i < to; i++) {
result.push(nodes[i]);
}
return result;
return Array.from(nodes).slice(from, to);
}
/* parentElements.childNodes is not an array. A findIndex() for it. */
/**
* node.childNodes is some funky data structure that's not really not an array,
* so we can't just search for the value like normal
*/
function findNodeIndex(nodes, element) {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i] === element) {
return i;
}
}
return -1;
if (!element)
return -1;
return Array.from(nodes).indexOf(element);
}

@@ -1121,0 +1158,0 @@ /* JSX Types */

{
"name": "forgo",
"version": "2.2.3",
"version": "3.0.0",
"main": "./dist/index.js",

@@ -17,2 +17,3 @@ "type": "module",

"@types/should": "^13.0.0",
"@types/source-map-support": "^0.5.4",
"esbuild": "^0.14.25",

@@ -23,2 +24,3 @@ "jsdom": "^19.0.0",

"should": "^13.2.3",
"source-map-support": "^0.5.21",
"typescript": "^4.6.2"

@@ -25,0 +27,0 @@ },

@@ -588,2 +588,68 @@ # forgo

## Manually adding elements to the DOM
Forgo allows you to insert elements into the DOM, inside a Forgo component,
using the vanilla JS DOM API. These elements will be completely unmanaged and
ignored by Forgo. This is useful for enabling charting libraries, such as D3.
If you add unmanaged elements as siblings to managed nodes, Forgo pushes the
unmanaged nodes towards the bottom of the sibling list as managed nodes are
added and removed. If you don't add/remove managed nodes, the unmanaged nodes
will stay in their original positions.
### ApexCharts example
[Code Sandbox](https://codesandbox.io/s/forgo-apexcharts-demo-ulkqfe?file=/src/index.tsx) for this example
```jsx
const App = () => {
const chartElement = {};
let interval;
return {
mount(_props, args) {
const chartOptions = {
chart: {
type: "line"
},
series: [
{
name: "sales",
data: [30, 40, 35, 50, 49, 60, 70, 91, 125]
}
],
xaxis: {
categories: [1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999]
}
};
const chart = new ApexCharts(chartElement.value, chartOptions);
chart.render();
interval = setInterval(() => args.update(), 1_000);
},
unmount() {
if (interval) clearInterval(interval);
},
render(_props, args) {
const now = new Date();
return (
<div>
<p>
This component continually rerenders. Forgo manages the timestamp,
but delegates control of the chart to ApexCharts.
</p>
<div ref={chartElement}></div>
<p>
The current time is:{" "}
<time datetime={now.toISOString()}>{now.toLocaleString()}</time>
</p>
</div>
);
}
};
};
```
## Try it out on CodeSandbox

@@ -590,0 +656,0 @@

@@ -133,2 +133,6 @@ /*

/**
* Anything renderable by Forgo, whether from an external source (e.g.,
* component.render() output), or internally (e.g., DOM nodes)
*/
export type ForgoNode = ForgoPrimitiveNode | ForgoElement<any> | ForgoFragment;

@@ -202,7 +206,5 @@

/*
NodeInsertionOptions decide how nodes get attached by the callee function.
type = "detached" does not attach the node to the parent.
type = "search" requires the callee function to search for a compatible replacement.
*/
/**
* Nodes will be created as detached DOM nodes, and will not be attached to the parent
*/
export type DetachedNodeInsertionOptions = {

@@ -212,9 +214,26 @@ type: "detached";

/**
* Instructs the renderer to search for an existing node to modify or replace,
* before creating a new node.
*/
export type SearchableNodeInsertionOptions = {
type: "search";
/**
* The element that holds the previously-rendered version of this component
*/
parentElement: Element;
/**
* Where under the parent's children to find the start of this component
*/
currentNodeIndex: number;
/**
* How many elements after currentNodeIndex belong to the element we're
* searching
*/
length: number;
};
/**
* Decides how the called function attaches nodes to the supplied parent
*/
export type NodeInsertionOptions =

@@ -270,4 +289,7 @@ | DetachedNodeInsertionOptions

/*
The element types we care about.
As defined by the standards.
These come from the browser's Node interface, which defines an enum of node
types. We'd like to just reference Node.<whatever>, but JSDOM makes us jump
through hoops to do that because it hates adding new globals. Getting around
that is more complex, and more bytes on the wire, than just hardcoding the
constants we care about.
*/

@@ -311,2 +333,6 @@ const ELEMENT_NODE_TYPE = 1;

/**
* Creates everything needed to run forgo, wrapped in a closure holding e.g.,
* JSDOM-specific environment overrides used in tests
*/
export function createForgoInstance(customEnv: any) {

@@ -319,9 +345,13 @@ const env: ForgoEnvType = customEnv;

/*
This is the main render function. forgoNode is the node to render.
insertionOptions specify which nodes need to be replaced by the new node(s),
or whether the new node should be created detached from the DOM (without replacement).
pendingAttachStates is the list of Component State objects which will be attached to the element.
*/
/**
* This is the main render function.
* @param forgoNode The node to render. Can be any value renderable by Forgo,
* not just DOM nodes.
* @param insertionOptions Which nodes need to be replaced by the new
* node(s), or whether the new node should be created detached from the DOM
* (without replacement).
* @param pendingAttachStates The list of Component State objects which will
* be attached to the element.
*/
function internalRender(

@@ -389,7 +419,7 @@ forgoNode: ForgoNode | ForgoNode[],

function renderNothing(
forgoNode: undefined | null,
insertionOptions: NodeInsertionOptions,
pendingAttachStates: NodeAttachedComponentState<any>[],
mountOnPreExistingDOM: boolean
) {
_forgoNode: undefined | null,
_insertionOptions: NodeInsertionOptions,
_pendingAttachStates: NodeAttachedComponentState<any>[],
_mountOnPreExistingDOM: boolean
): RenderResult {
return { nodes: [] };

@@ -414,81 +444,52 @@ }

pendingAttachStates: NodeAttachedComponentState<any>[],
mountOnPreExistingDOM: boolean
// TODO: Any reason to keep this parameter?
_mountOnPreExistingDOM: boolean
): RenderResult {
// We need to create a detached node.
if (insertionOptions.type === "detached") {
// Text nodes will always be recreated.
const textNode: ChildNode = env.document.createTextNode(
stringOfPrimitiveNode(forgoNode)
);
syncStateAndProps(
forgoNode,
textNode,
true,
pendingAttachStates,
undefined
);
return { nodes: [textNode] };
}
// Text nodes will always be recreated.
const textNode: ChildNode = env.document.createTextNode(
stringOfPrimitiveNode(forgoNode)
);
let oldComponentState: NodeAttachedComponentState<any>[] | undefined =
undefined;
// We have to find a node to replace.
else {
// Text nodes will always be recreated.
const textNode: ChildNode = env.document.createTextNode(
stringOfPrimitiveNode(forgoNode)
);
if (insertionOptions.type === "search") {
const childNodes = insertionOptions.parentElement.childNodes;
// If we're searching in a list, we replace if the current node is a text node.
const childNodes = insertionOptions.parentElement.childNodes;
if (insertionOptions.length) {
let targetNode = childNodes[insertionOptions.currentNodeIndex];
const targetNode = childNodes[insertionOptions.currentNodeIndex];
if (targetNode.nodeType === TEXT_NODE_TYPE) {
targetNode.replaceWith(textNode);
const oldComponentState = getForgoState(targetNode)?.components;
syncStateAndProps(
forgoNode,
textNode,
true,
pendingAttachStates,
oldComponentState
);
return { nodes: [textNode] };
oldComponentState = getForgoState(targetNode)?.components;
} else {
const nextNode = childNodes[insertionOptions.currentNodeIndex];
insertionOptions.parentElement.insertBefore(textNode, nextNode);
syncStateAndProps(
forgoNode,
textNode,
true,
pendingAttachStates,
undefined
);
return { nodes: [textNode] };
}
}
// There are no target nodes available.
else {
const childNodes = insertionOptions.parentElement.childNodes;
if (
childNodes.length === 0 ||
insertionOptions.currentNodeIndex === 0
) {
insertionOptions.parentElement.prepend(textNode);
} else {
const nextNode = childNodes[insertionOptions.currentNodeIndex];
insertionOptions.parentElement.insertBefore(textNode, nextNode);
}
syncStateAndProps(
forgoNode,
textNode,
true,
pendingAttachStates,
undefined
);
return { nodes: [textNode] };
else if (
childNodes.length === 0 ||
insertionOptions.currentNodeIndex === 0
) {
insertionOptions.parentElement.prepend(textNode);
} else {
const nextNode = childNodes[insertionOptions.currentNodeIndex];
insertionOptions.parentElement.insertBefore(textNode, nextNode);
}
}
syncStateAndProps(
forgoNode,
textNode,
true,
pendingAttachStates,
oldComponentState
);
return { nodes: [textNode] };
}
/*
Render a DOM element.
Render a DOM element. Will find + update an existing DOM element (if
appropriate), or insert a new element.

@@ -532,17 +533,14 @@ Such as in the render function below:

);
} else {
return addElement(
insertionOptions.parentElement,
childNodes[insertionOptions.currentNodeIndex]
);
}
} else {
return addElement(
insertionOptions.parentElement,
childNodes[insertionOptions.currentNodeIndex]
);
}
return addElement(
insertionOptions.parentElement,
childNodes[insertionOptions.currentNodeIndex]
);
}
function renderChildNodes(parentElement: Element) {
// If the user gave us exact HTML to stuff into this parent, we can
// skip/ignore the usual rendering logic
if (forgoElement.props.dangerouslySetInnerHTML) {

@@ -552,16 +550,22 @@ parentElement.innerHTML =

} else {
const forgoChildrenObj = forgoElement.props.children;
// Children will not be an array if single item.
const forgoChildren = flatten(
(Array.isArray(forgoChildrenObj)
? forgoChildrenObj
: [forgoChildrenObj]
).filter((x) => x !== undefined && x !== null)
// Coerce children to always be an array, for simplicity
const forgoChildren = flatten([forgoElement.props.children]).filter(
// Children may or may not be specified
(x) => x !== undefined && x !== null
);
let currentChildNodeIndex = 0;
// Make sure that if the user prepends non-Forgo DOM children under this
// parent that we start with the correct offset, otherwise we'll do DOM
// transformations that don't make any sense for the given input.
const firstForgoChildIndex = Array.from(
parentElement.childNodes
).findIndex((child) => getForgoState(child));
// Each node we render will push any leftover children further down the
// parent's list of children. After rendering everything, we can clean
// up anything extra. We'll know what's extra because all nodes we want
// to preserve come before this index.
let lastRenderedNodeIndex =
firstForgoChildIndex === -1 ? 0 : firstForgoChildIndex;
for (const forgoChild of forgoChildren) {
const { nodes } = internalRender(
const { nodes: nodesAfterRender } = internalRender(
forgoChild,

@@ -571,4 +575,4 @@ {

parentElement,
currentNodeIndex: currentChildNodeIndex,
length: parentElement.childNodes.length - currentChildNodeIndex,
currentNodeIndex: lastRenderedNodeIndex,
length: parentElement.childNodes.length - lastRenderedNodeIndex,
},

@@ -578,18 +582,51 @@ [],

);
currentChildNodeIndex += nodes.length;
// Continue down the children list to wherever's right after the stuff
// we just added. Because users are allowed to add arbitrary stuff to
// the DOM manually, we can't just jump by the count of rendered
// elements, since that's the count of *managed* elements, which might
// be interspersed with unmanaged elements that we also need to skip
// past.
if (nodesAfterRender.length) {
while (
parentElement.childNodes[lastRenderedNodeIndex] !==
nodesAfterRender[nodesAfterRender.length - 1]
) {
lastRenderedNodeIndex += 1;
}
// Move the counter *past* the last node we inserted. E.g., if we just
// inserted our first node, we need to increment from 0 -> 1, where
// we'll start searching for the next thing we insert
lastRenderedNodeIndex += 1;
// If we're updating an existing DOM element, it's possible that the
// user manually added some DOM nodes somewhere in the middle of our
// managed nodes. If that happened, we need to scan forward until we
// pass them and find the next managed node, which we'll use as the
// starting point for whatever we render next. We still need the +1
// above to make sure we always progress the index, in case this is
// our first render pass and there's nothing to scan forward to.
while (lastRenderedNodeIndex < parentElement.childNodes.length) {
if (
getForgoState(parentElement.childNodes[lastRenderedNodeIndex])
) {
break;
}
lastRenderedNodeIndex += 1;
}
}
}
// Get rid the the remaining nodes.
const nodesToRemove = sliceNodes(
// Remove all nodes that don't correspond to the rendered output of a
// live component
markNodesForUnloading(
parentElement.childNodes,
currentChildNodeIndex,
lastRenderedNodeIndex,
parentElement.childNodes.length
);
if (nodesToRemove.length) {
markNodesForUnloading(nodesToRemove);
}
}
}
/**
* If we're updating an element that was rendered in a previous render,
* reuse the same DOM element. Just sync its children and attributes.
*/
function renderExistingElement(

@@ -601,3 +638,3 @@ insertAt: number,

// Get rid of unwanted nodes.
const nodesToRemove = sliceNodes(
markNodesForUnloading(
childNodes,

@@ -607,3 +644,2 @@ insertionOptions.currentNodeIndex,

);
markNodesForUnloading(nodesToRemove);

@@ -671,40 +707,29 @@ const targetElement = childNodes[

// We need to create a detached node.
if (insertionOptions.type === "detached") {
return addComponent();
}
// We have to find a node to replace.
else {
if (insertionOptions.length) {
if (mountOnPreExistingDOM) {
return addComponent();
} else {
const childNodes = insertionOptions.parentElement.childNodes;
const searchResult = findReplacementCandidateForComponent(
forgoElement,
insertionOptions.parentElement,
insertionOptions.currentNodeIndex,
insertionOptions.length,
pendingAttachStates.length
);
if (
// We need to create a detached node.
insertionOptions.type !== "detached" &&
// We have to find a node to replace.
insertionOptions.length &&
!mountOnPreExistingDOM
) {
const childNodes = insertionOptions.parentElement.childNodes;
const searchResult = findReplacementCandidateForComponent(
forgoElement,
insertionOptions.parentElement,
insertionOptions.currentNodeIndex,
insertionOptions.length,
pendingAttachStates.length
);
if (searchResult.found) {
return renderExistingComponent(
searchResult.index,
childNodes,
insertionOptions
);
}
// No matching node found.
else {
return addComponent();
}
}
if (searchResult.found) {
return renderExistingComponent(
searchResult.index,
childNodes,
insertionOptions
);
}
// No nodes in target node list.
// Nothing to unload.
else {
return addComponent();
}
}
// No nodes in target node list, or no matching node found.
// Nothing to unload.
return addComponent();

@@ -721,3 +746,3 @@ function renderExistingComponent(

// Get rid of unwanted nodes.
const nodesToRemove = sliceNodes(
markNodesForUnloading(
childNodes,

@@ -727,7 +752,6 @@ insertionOptions.currentNodeIndex,

);
markNodesForUnloading(nodesToRemove);
if (
!componentState.component.shouldUpdate ||
componentState.component.shouldUpdate(
!componentState.component.shouldUpdate(
forgoElement.props,

@@ -934,3 +958,3 @@ componentState.props

const nodesToRemove = sliceNodes(
markNodesForUnloading(
insertionOptions.parentElement.childNodes,

@@ -941,4 +965,2 @@ newIndex,

markNodesForUnloading(nodesToRemove);
// In case we rendered an array, set the node to the first node.

@@ -1048,13 +1070,31 @@ // We do this because args.element.node would be set to the last node otherwise.

/*
This doesn't unmount components attached to these nodes,
but moves the node itself from the DOM to parentNode.__forgo_deletedNodes.
We sort of "mark" it for deletion, but it may be resurrected when a keyed,
off-order forgo node matches a deleted node.
*/
function markNodesForUnloading(nodes: ChildNode[]) {
if (nodes.length) {
const parentElement = nodes[0].parentElement as HTMLElement;
/**
* This doesn't unmount components attached to these nodes, but moves the node
* itself from the DOM to parentNode.__forgo_deletedNodes. We sort of "mark"
* it for deletion, but it may be resurrected if it's matched by a keyed forgo
* node that has been reordered.
*
* Nodes in between `from` and `to` (not inclusive of `to`) will be marked for
* unloading. Use `unloadMarkedNodes()` to actually unload the nodes once
* we're sure we don't need to resurrect them.
*
* We don't want to remove DOM nodes that aren't owned by Forgo. I.e., if the
* user grabs a reference to a DOM element and manually adds children under
* it, we don't want to remove those children. That'll mess up e.g., charting
* libraries.
*/
function markNodesForUnloading(
nodes: ArrayLike<ChildNode>,
from: number,
to: number
) {
const nodesToRemove = sliceNodes(nodes, from, to);
if (nodesToRemove.length) {
const parentElement = nodesToRemove[0].parentElement as HTMLElement;
const deletedNodes = getDeletedNodes(parentElement);
for (const node of nodes) {
for (const node of nodesToRemove) {
// If the consuming application has manually mucked with the DOM don't
// remove things it added
if (!getForgoState(node)) continue;
node.remove();

@@ -1192,8 +1232,8 @@ deletedNodes.push({ node });

/*
When we try to find replacement candidates for DOM nodes,
we try to:
a) match by the key
b) match by the tagname
*/
/**
* When we try to find replacement candidates for DOM nodes,
* we try to:
* a) match by the key
* b) match by the tagname
*/
function findReplacementCandidateForElement<TProps>(

@@ -1210,6 +1250,12 @@ forgoElement: ForgoDOMElement<TProps>,

const stateOnNode = getForgoState(node);
if (forgoElement.key) {
if (stateOnNode?.key === forgoElement.key) {
return { found: true, index: i };
}
// If the user stuffs random elements into the DOM manually, we don't
// want to treat them as replacement candidates - they should be left
// alone.
if (!stateOnNode) continue;
if (
forgoElement.key !== undefined &&
stateOnNode?.key === forgoElement.key
) {
return { found: true, index: i };
} else {

@@ -1220,3 +1266,3 @@ // If the candidate has a key defined,

node.tagName.toLowerCase() === forgoElement.type &&
(!stateOnNode || !stateOnNode.key)
!stateOnNode?.key
) {

@@ -1229,3 +1275,3 @@ return { found: true, index: i };

// Let's check deleted nodes as well.
if (forgoElement.key) {
if (forgoElement.key !== undefined) {
const deletedNodes = getDeletedNodes(parentElement);

@@ -1252,8 +1298,8 @@ for (let i = 0; i < deletedNodes.length; i++) {

/*
When we try to find replacement candidates for Components,
we try to:
a) match by the key
b) match by the component constructor
*/
/**
* When we try to find replacement candidates for Components,
* we try to:
* a) match by the key
* b) match by the component constructor
*/
function findReplacementCandidateForComponent<TProps>(

@@ -1271,3 +1317,3 @@ forgoElement: ForgoComponentElement<TProps>,

if (stateOnNode && stateOnNode.components.length > componentIndex) {
if (forgoElement.key) {
if (forgoElement.key !== undefined) {
if (

@@ -1297,3 +1343,6 @@ stateOnNode.components[componentIndex].ctor === forgoElement.type &&

if (stateOnNode && stateOnNode.components.length > componentIndex) {
if (stateOnNode.components[componentIndex].ctor === forgoElement.type && stateOnNode.components[componentIndex].key === forgoElement.key) {
if (
stateOnNode.components[componentIndex].ctor === forgoElement.type &&
stateOnNode.components[componentIndex].key === forgoElement.key
) {
return true;

@@ -1306,3 +1355,3 @@ }

// Let's check deleted nodes as well.
if (forgoElement.key) {
if (forgoElement.key !== undefined) {
const deletedNodes = getDeletedNodes(parentElement);

@@ -1312,7 +1361,3 @@ for (let i = 0; i < deletedNodes.length; i++) {

if (
nodeBelongsToKeyedComponent(
deletedNode,
forgoElement,
componentIndex
)
nodeBelongsToKeyedComponent(deletedNode, forgoElement, componentIndex)
) {

@@ -1357,6 +1402,6 @@ const nodesToResurrect: ChildNode[] = [deletedNode];

/*
Attach props from the forgoElement on to the DOM node.
We also need to attach states from pendingAttachStates
*/
/**
* Attach props from the forgoElement onto the DOM node. We also need to attach
* states from pendingAttachStates
*/
function attachProps(

@@ -1572,4 +1617,3 @@ forgoNode: ForgoNode,

const effectiveProps =
props !== undefined ? props : originalComponentState.props;
const effectiveProps = props ?? originalComponentState.props;

@@ -1908,3 +1952,6 @@ if (

/* parentElements.childNodes is not an array. A slice() for it. */
/**
* node.childNodes is some funky data structure that's not really not an array,
* so we can't just slice it like normal
*/
function sliceNodes(

@@ -1915,10 +1962,9 @@ nodes: ArrayLike<ChildNode>,

): ChildNode[] {
const result: ChildNode[] = [];
for (let i = from; i < to; i++) {
result.push(nodes[i]);
}
return result;
return Array.from(nodes).slice(from, to);
}
/* parentElements.childNodes is not an array. A findIndex() for it. */
/**
* node.childNodes is some funky data structure that's not really not an array,
* so we can't just search for the value like normal
*/
function findNodeIndex(

@@ -1928,8 +1974,4 @@ nodes: ArrayLike<ChildNode>,

): number {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i] === element) {
return i;
}
}
return -1;
if (!element) return -1;
return Array.from(nodes).indexOf(element);
}

@@ -1936,0 +1978,0 @@

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc