Comparing version 4.0.6 to 4.1.0
@@ -6,2 +6,4 @@ export type ForgoRef<T> = { | ||
children?: ForgoNode | ForgoNode[]; | ||
}; | ||
export type ForgoDOMElementProps = { | ||
xmlns?: string; | ||
@@ -12,22 +14,20 @@ ref?: ForgoRef<Element>; | ||
}; | ||
}; | ||
export type ForgoComponentCtor<Props extends object = object> = (props: Props & ForgoElementProps) => ForgoComponent<Props>; | ||
export type ForgoNewComponentCtor<Props extends object = object> = (props: Props & ForgoElementProps) => Component<Props>; | ||
} & ForgoElementProps; | ||
export type ForgoComponentProps = ForgoElementProps; | ||
export type ForgoComponentCtor<Props extends {} = {}> = (props: Props & ForgoComponentProps) => ForgoComponent<Props>; | ||
export type ForgoNewComponentCtor<Props extends {} = {}> = (props: Props & ForgoComponentProps) => Component<Props>; | ||
export type ForgoElementArg = { | ||
node?: ChildNode; | ||
nodeIndex: number; | ||
componentIndex: number; | ||
}; | ||
export type ForgoKeyType = string | number; | ||
export type ForgoDOMElement<Props extends object> = { | ||
key?: ForgoKeyType; | ||
props: Props & ForgoElementProps; | ||
export type ForgoElementBase<TProps extends ForgoElementProps> = { | ||
key?: any; | ||
props: TProps; | ||
__is_forgo_element__: true; | ||
}; | ||
export type ForgoDOMElement<TProps extends ForgoDOMElementProps> = ForgoElementBase<TProps> & { | ||
type: string; | ||
}; | ||
export type ForgoComponentElement<Props extends object> = { | ||
key?: ForgoKeyType; | ||
props: Props & ForgoElementProps; | ||
__is_forgo_element__: true; | ||
type: ForgoNewComponentCtor<Props>; | ||
export type ForgoComponentElement<TProps extends ForgoComponentProps> = ForgoElementBase<TProps> & { | ||
type: ForgoNewComponentCtor<TProps>; | ||
}; | ||
@@ -41,4 +41,4 @@ export type ForgoFragment = { | ||
}; | ||
export type ForgoElement<Props extends object> = ForgoDOMElement<Props> | ForgoComponentElement<Props>; | ||
export type ForgoNonEmptyPrimitiveNode = string | number | boolean | object | bigint | null | undefined; | ||
export type ForgoElement<TProps extends ForgoDOMElementProps> = ForgoDOMElement<TProps> | ForgoComponentElement<TProps>; | ||
export type ForgoNonEmptyPrimitiveNode = string | number | boolean | object | BigInt | null | undefined; | ||
export type ForgoPrimitiveNode = ForgoNonEmptyPrimitiveNode | null | undefined; | ||
@@ -50,7 +50,7 @@ /** | ||
export type ForgoNode = ForgoPrimitiveNode | ForgoElement<any> | ForgoFragment; | ||
export type ComponentState<Props extends object = object> = { | ||
key?: string | number; | ||
ctor: ForgoNewComponentCtor<Props> | ForgoComponentCtor<Props>; | ||
component: Component<Props>; | ||
props: Props & ForgoElementProps; | ||
export type NodeAttachedComponentState<TProps extends {}> = { | ||
key?: any; | ||
ctor: ForgoNewComponentCtor<TProps> | ForgoComponentCtor<TProps>; | ||
component: Component<TProps>; | ||
props: TProps; | ||
nodes: ChildNode[]; | ||
@@ -64,3 +64,3 @@ isMounted: boolean; | ||
}; | ||
components: ComponentState<any>[]; | ||
components: NodeAttachedComponentState<any>[]; | ||
style?: { | ||
@@ -70,12 +70,2 @@ [key: string]: any; | ||
deleted?: boolean; | ||
lookups: { | ||
deletedUnkeyedNodes: DeletedNode[]; | ||
deletedKeyedComponentNodes: Map<string | number, ChildNode[]>; | ||
keyedComponentNodes: Map<string | number, ChildNode[]>; | ||
newlyAddedKeyedComponentNodes: Map<string | number, ChildNode[]>; | ||
deletedKeyedElementNodes: Map<string | number, ChildNode>; | ||
keyedElementNodes: Map<string | number, ChildNode>; | ||
newlyAddedKeyedElementNodes: Map<string | number, ChildNode>; | ||
renderCount: number; | ||
}; | ||
}; | ||
@@ -100,3 +90,3 @@ export type DOMCSSProperties = { | ||
/** | ||
* Nodes will be created as detached DOM nodes, and will not be attached to a parent. | ||
* Nodes will be created as detached DOM nodes, and will not be attached to the parent | ||
*/ | ||
@@ -110,3 +100,3 @@ export type DetachedNodeInsertionOptions = { | ||
*/ | ||
export type DOMNodeInsertionOptions = { | ||
export type SearchableNodeInsertionOptions = { | ||
type: "search"; | ||
@@ -127,5 +117,13 @@ /** | ||
}; | ||
export type NodeInsertionOptions = DetachedNodeInsertionOptions | DOMNodeInsertionOptions; | ||
/** | ||
* Decides how the called function attaches nodes to the supplied parent | ||
*/ | ||
export type NodeInsertionOptions = DetachedNodeInsertionOptions | SearchableNodeInsertionOptions; | ||
export type UnloadableChildNode = { | ||
node: ChildNode; | ||
pendingAttachStates: NodeAttachedComponentState<any>[]; | ||
}; | ||
export type RenderResult = { | ||
nodes: ChildNode[]; | ||
pendingMounts: (() => void)[]; | ||
}; | ||
@@ -138,8 +136,16 @@ export type DeletedNode = { | ||
__forgo?: NodeAttachedState; | ||
__forgo_deletedNodes?: DeletedNode[]; | ||
} | ||
} | ||
export declare const Fragment: unique symbol; | ||
export interface ForgoComponentMethods<Props extends object> { | ||
render: (props: Props & ForgoElementProps, component: Component<Props>) => ForgoNode | ForgoNode[]; | ||
error?: (props: Props & ForgoElementProps, error: unknown, component: Component<Props>) => ForgoNode; | ||
/** | ||
* These are methods that a component may implement. Every component is required | ||
* to have a render method. | ||
* 1. render() returns the actual DOM to render. | ||
* 2. error() is called when this component, or one of its children, throws an | ||
* error. | ||
*/ | ||
export interface ForgoComponentMethods<Props extends ForgoComponentProps> { | ||
render: (props: Props & ForgoComponentProps, component: Component<Props>) => ForgoNode | ForgoNode[]; | ||
error?: (props: Props & ForgoComponentProps, error: unknown, component: Component<Props>) => ForgoNode; | ||
} | ||
@@ -159,10 +165,9 @@ /** | ||
*/ | ||
interface ComponentEventListeners<Props extends object> extends ComponentEventListenerBase { | ||
mount: Array<(props: Props & ForgoElementProps, component: Component<Props>) => any>; | ||
remount: Array<(props: Props & ForgoElementProps, component: Component<Props>) => any>; | ||
unmount: Array<(props: Props & ForgoElementProps, component: Component<Props>) => any>; | ||
afterRender: Array<(props: Props & ForgoElementProps, previousNode: ChildNode | undefined, component: Component<Props>) => any>; | ||
shouldUpdate: Array<(newProps: Props & ForgoElementProps, oldProps: Props & ForgoElementProps, component: Component<Props>) => boolean>; | ||
interface ComponentEventListeners<Props extends {}> extends ComponentEventListenerBase { | ||
mount: Array<(props: Props & ForgoComponentProps, component: Component<Props>) => void>; | ||
unmount: Array<(props: Props & ForgoComponentProps, component: Component<Props>) => void>; | ||
afterRender: Array<(props: Props & ForgoComponentProps, previousNode: ChildNode | undefined, component: Component<Props>) => void>; | ||
shouldUpdate: Array<(newProps: Props & ForgoComponentProps, oldProps: Props & ForgoComponentProps, component: Component<Props>) => boolean>; | ||
} | ||
interface ComponentInternal<Props extends object = object> { | ||
interface ComponentInternal<Props extends {}> { | ||
unmounted: boolean; | ||
@@ -174,7 +179,6 @@ registeredMethods: ForgoComponentMethods<Props>; | ||
declare const lifecycleEmitters: { | ||
mount<Props extends object = object>(component: Component<Props>, props: Props & ForgoElementProps): void; | ||
remount<Props_1 extends object = object>(component: Component<Props_1>, props: Props_1 & ForgoElementProps): void; | ||
unmount<Props_2 extends object = object>(component: Component<Props_2>, props: Props_2 & ForgoElementProps): void; | ||
shouldUpdate<Props_3 extends object = object>(component: Component<Props_3>, newProps: Props_3 & ForgoElementProps, oldProps: Props_3 & ForgoElementProps): boolean; | ||
afterRender<Props_4 extends object = object>(component: Component<Props_4>, props: Props_4 & ForgoElementProps, previousNode: ChildNode | undefined): void; | ||
mount<Props extends {}>(component: Component<Props>, props: Props): void; | ||
unmount<Props_1 extends {}>(component: Component<Props_1>, props: Props_1): void; | ||
shouldUpdate<Props_2 extends {}>(component: Component<Props_2>, newProps: Props_2, oldProps: Props_2): boolean; | ||
afterRender<Props_3 extends {}>(component: Component<Props_3>, props: Props_3, previousNode: ChildNode | undefined): void; | ||
}; | ||
@@ -186,3 +190,3 @@ /** | ||
*/ | ||
export declare class Component<Props extends object = object> { | ||
export declare class Component<Props extends {} = {}> { | ||
/** @internal */ | ||
@@ -198,3 +202,2 @@ __internal: ComponentInternal<Props>; | ||
mount(listener: ComponentEventListeners<Props>["mount"][number]): void; | ||
remount(listener: ComponentEventListeners<Props>["remount"][number]): void; | ||
unmount(listener: ComponentEventListeners<Props>["unmount"][number]): void; | ||
@@ -207,7 +210,7 @@ shouldUpdate(listener: ComponentEventListeners<Props>["shouldUpdate"][number]): void; | ||
*/ | ||
export declare function createElement<Props extends object & { | ||
export declare function createElement<TProps extends ForgoElementProps & { | ||
key?: any; | ||
}>(type: string | ForgoNewComponentCtor<Props> | ForgoComponentCtor<Props>, props: Props & ForgoElementProps, ...args: any[]): { | ||
type: string | ForgoNewComponentCtor<Props> | ForgoComponentCtor<Props>; | ||
props: Props & ForgoElementProps; | ||
}>(type: string | ForgoNewComponentCtor<TProps> | ForgoComponentCtor<TProps>, props: TProps): { | ||
type: string | ForgoNewComponentCtor<TProps> | ForgoComponentCtor<TProps>; | ||
props: TProps; | ||
key: any; | ||
@@ -245,3 +248,3 @@ __is_forgo_element__: boolean; | ||
export declare function rerender(element: ForgoElementArg | undefined, props?: any): RenderResult; | ||
export declare function getForgoState(node: ChildNode): NodeAttachedState; | ||
export declare function getForgoState(node: ChildNode): NodeAttachedState | undefined; | ||
export declare function setForgoState(node: ChildNode, state: NodeAttachedState): void; | ||
@@ -251,10 +254,9 @@ /** | ||
*/ | ||
export type ForgoComponent<Props extends object = object> = { | ||
render: (props: Props & ForgoElementProps, args: ForgoRenderArgs) => ForgoNode | ForgoNode[]; | ||
afterRender?: (props: Props & ForgoElementProps, args: ForgoAfterRenderArgs) => void; | ||
error?: (props: Props & ForgoElementProps, args: ForgoErrorArgs) => ForgoNode; | ||
mount?: (props: Props & ForgoElementProps, args: ForgoRenderArgs) => void; | ||
remount?: (props: Props & ForgoElementProps, args: ForgoRenderArgs) => void; | ||
unmount?: (props: Props & ForgoElementProps, args: ForgoRenderArgs) => void; | ||
shouldUpdate?: (newProps: Props & ForgoElementProps, oldProps: Props & ForgoElementProps) => boolean; | ||
export type ForgoComponent<TProps extends ForgoComponentProps> = { | ||
render: (props: TProps, args: ForgoRenderArgs) => ForgoNode | ForgoNode[]; | ||
afterRender?: (props: TProps, args: ForgoAfterRenderArgs) => void; | ||
error?: (props: TProps, args: ForgoErrorArgs) => ForgoNode; | ||
mount?: (props: TProps, args: ForgoRenderArgs) => void; | ||
unmount?: (props: TProps, args: ForgoRenderArgs) => void; | ||
shouldUpdate?: (newProps: TProps, oldProps: TProps) => boolean; | ||
__forgo?: { | ||
@@ -274,3 +276,3 @@ unmounted?: boolean; | ||
}; | ||
export declare const legacyComponentSyntaxCompat: <Props extends object = object>(legacyComponent: ForgoComponent<Props>) => Component<Props>; | ||
export declare const legacyComponentSyntaxCompat: <Props extends {}>(legacyComponent: ForgoComponent<Props>) => Component<Props>; | ||
export * as JSX from "./jsxTypes.js"; | ||
@@ -277,0 +279,0 @@ import * as JSXTypes from "./jsxTypes.js"; |
@@ -5,21 +5,19 @@ // Since we'll set any attribute the user passes us, we need to be sure not to | ||
/* | ||
* Fragment constructor. | ||
* We simply use it as a marker in jsx-runtime. | ||
*/ | ||
Fragment constructor. | ||
We simply use it as a marker in jsx-runtime. | ||
*/ | ||
export const Fragment = Symbol.for("FORGO_FRAGMENT"); | ||
/* | ||
* HTML Namespaces | ||
*/ | ||
const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
HTML Namespaces | ||
*/ | ||
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const MATH_NAMESPACE = "http://www.w3.org/1998/Math/MathML"; | ||
const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; | ||
/* | ||
* 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. | ||
*/ | ||
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. | ||
*/ | ||
const ELEMENT_NODE_TYPE = 1; | ||
@@ -32,5 +30,2 @@ const TEXT_NODE_TYPE = 3; | ||
}, | ||
remount(component, props) { | ||
component.__internal.eventListeners.remount.forEach((cb) => cb(props, component)); | ||
}, | ||
unmount(component, props) { | ||
@@ -69,7 +64,6 @@ component.__internal.eventListeners.unmount.forEach((cb) => cb(props, component)); | ||
mount: [], | ||
remount: [], | ||
unmount: [], | ||
shouldUpdate: [], | ||
}, | ||
element: { componentIndex: -1, nodeIndex: -1 }, | ||
element: { componentIndex: -1 }, | ||
}; | ||
@@ -86,5 +80,2 @@ } | ||
} | ||
remount(listener) { | ||
this.__internal.eventListeners["remount"].push(listener); | ||
} | ||
unmount(listener) { | ||
@@ -103,10 +94,10 @@ this.__internal.eventListeners["unmount"].push(listener); | ||
*/ | ||
export function createElement(type, props, ...args) { | ||
export function createElement(type, props) { | ||
var _a; | ||
props = props !== null && props !== void 0 ? props : {}; | ||
props.children = | ||
args.length > 1 | ||
? flatten(Array.from(args)) | ||
: args.length === 1 | ||
? flatten(args[0]) | ||
arguments.length > 3 | ||
? flatten(Array.from(arguments).slice(2)) | ||
: arguments.length === 3 | ||
? flatten(arguments[2]) | ||
: undefined; | ||
@@ -118,9 +109,10 @@ const key = (_a = props.key) !== null && _a !== void 0 ? _a : undefined; | ||
/* | ||
* HACK: Chrome fires onblur (if defined) immediately after a node.remove(). | ||
* This is bad news for us, since a rerender() inside the onblur handler will | ||
* run on an unattached node. So, disable onblur if node is set to be removed. | ||
*/ | ||
HACK: Chrome fires onblur (if defined) immediately after a node.remove(). | ||
This is bad news for us, since a rerender() inside the onblur handler | ||
will run on an unattached node. So, disable onblur if node is set to be removed. | ||
*/ | ||
function handlerDisabledOnNodeDelete(node, value) { | ||
return (e) => { | ||
if (node.__forgo === undefined || node.__forgo.deleted === false) { | ||
var _a; | ||
if (!((_a = node.__forgo) === null || _a === void 0 ? void 0 : _a.deleted)) { | ||
return value(e); | ||
@@ -142,12 +134,12 @@ } | ||
/** | ||
* 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. | ||
*/ | ||
* 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) { | ||
@@ -168,5 +160,28 @@ // Array of Nodes, or Fragment | ||
else { | ||
return renderComponent(forgoNode, insertionOptions, pendingAttachStates, mountOnPreExistingDOM); | ||
const result = renderComponent(forgoNode, insertionOptions, pendingAttachStates, mountOnPreExistingDOM); | ||
// In order to prevent issue #50 (Fragments having mount() called before | ||
// *all* child elements have finished rendering), we delay calling mount | ||
// until a subtree's render has completed | ||
// | ||
// Ideally this would encompass both mounts and unmounts, but an unmounted | ||
// component doesn't get `renderComponent()` called on it, so we need to | ||
// continue unmounting inside each of the type-specific render functions. | ||
// That's fine since the problem is elements not existing at mount time, | ||
// whereas unmount timing isn't sensitive to that. | ||
result.pendingMounts.forEach((fn) => fn()); | ||
result.pendingMounts.length = 0; | ||
return result; | ||
} | ||
} | ||
/* | ||
Render a string. | ||
* Such as in the render function below: | ||
* function MyComponent() { | ||
* return new forgo.Component({ | ||
* render() { | ||
* return "Hello world" | ||
* } | ||
* }) | ||
* } | ||
*/ | ||
function renderNonElement(forgoNode, insertionOptions, pendingAttachStates) { | ||
@@ -176,3 +191,3 @@ var _a; | ||
let node; | ||
if (isNullOrUndefined(forgoNode)) { | ||
if (forgoNode === null || forgoNode === undefined) { | ||
node = env.document.createComment("null component render"); | ||
@@ -197,3 +212,3 @@ } | ||
const nextNode = childNodes[insertionOptions.currentNodeIndex]; | ||
insertionOptions.parentElement.insertBefore(node, nextNode !== null && nextNode !== void 0 ? nextNode : null); | ||
insertionOptions.parentElement.insertBefore(node, nextNode); | ||
} | ||
@@ -208,17 +223,31 @@ } | ||
const nextNode = childNodes[insertionOptions.currentNodeIndex]; | ||
insertionOptions.parentElement.insertBefore(node, nextNode !== null && nextNode !== void 0 ? nextNode : null); | ||
insertionOptions.parentElement.insertBefore(node, nextNode); | ||
} | ||
syncAttrsAndState(forgoNode, node, insertionOptions.currentNodeIndex, true, pendingAttachStates); | ||
} | ||
else { | ||
syncAttrsAndState(forgoNode, node, -1, true, pendingAttachStates); | ||
} | ||
syncAttrsAndState(forgoNode, node, true, pendingAttachStates); | ||
unmountComponents(pendingAttachStates, oldComponentState); | ||
return { | ||
nodes: [node], | ||
pendingMounts: [ | ||
() => mountComponents(pendingAttachStates, oldComponentState), | ||
], | ||
}; | ||
} | ||
/* | ||
Render a DOM element. Will find + update an existing DOM element (if | ||
appropriate), or insert a new element. | ||
Such as in the render function below: | ||
function MyComponent() { | ||
return { | ||
render() { | ||
return <div>Hello world</div> | ||
} | ||
} | ||
} | ||
*/ | ||
function renderDOMElement(forgoElement, insertionOptions, pendingAttachStates, mountOnPreExistingDOM) { | ||
// We need to create a detached node | ||
if (insertionOptions.type === "detached") { | ||
return addElement(undefined, undefined); | ||
return addElement(undefined, null); | ||
} | ||
@@ -228,36 +257,71 @@ // We have to find a node to replace. | ||
const childNodes = insertionOptions.parentElement.childNodes; | ||
const found = findReplacementCandidateForElement(forgoElement, insertionOptions, pendingAttachStates); | ||
const renderResult = found | ||
? renderExistingElement(insertionOptions) | ||
: addElement(insertionOptions.parentElement, insertionOptions.currentNodeIndex); | ||
return renderResult; | ||
if (insertionOptions.length) { | ||
const searchResult = findReplacementCandidateForElement(forgoElement, insertionOptions.parentElement, insertionOptions.currentNodeIndex, insertionOptions.length); | ||
if (searchResult.found) { | ||
return renderExistingElement(searchResult.index, childNodes, insertionOptions); | ||
} | ||
} | ||
return addElement(insertionOptions.parentElement, childNodes[insertionOptions.currentNodeIndex]); | ||
} | ||
function renderChildNodes(element) { | ||
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) { | ||
element.innerHTML = forgoElement.props.dangerouslySetInnerHTML.__html; | ||
parentElement.innerHTML = | ||
forgoElement.props.dangerouslySetInnerHTML.__html; | ||
} | ||
else { | ||
const state = getForgoState(element); | ||
initKeyLookupLoop(state); | ||
// Coerce children to always be an array, for simplicity | ||
const forgoChildren = flatten([forgoElement.props.children]).filter( | ||
// Children may or may not be specified | ||
(x) => !isNullOrUndefined(x)); | ||
let currentNodeIndex = 0; | ||
(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: nodesJustRendered } = internalRender(forgoChild, { | ||
const { nodes: nodesAfterRender } = internalRender(forgoChild, { | ||
type: "search", | ||
parentElement: element, | ||
currentNodeIndex, | ||
length: element.childNodes.length - currentNodeIndex, | ||
parentElement, | ||
currentNodeIndex: lastRenderedNodeIndex, | ||
length: parentElement.childNodes.length - lastRenderedNodeIndex, | ||
}, [], mountOnPreExistingDOM); | ||
currentNodeIndex += nodesJustRendered.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; | ||
} | ||
} | ||
} | ||
// Clear nodes remaining after currentNodeIndex | ||
// eg: if currentNodeIndex = 10 (and length = 20), remove everything > 10 | ||
markNodesForUnloading(element.childNodes, currentNodeIndex, element.childNodes.length); | ||
unloadMarkedNodes(element); | ||
finalizeKeyLookups(state); | ||
// Remove all nodes that don't correspond to the rendered output of a | ||
// live component | ||
markNodesForUnloading(parentElement.childNodes, lastRenderedNodeIndex, parentElement.childNodes.length); | ||
} | ||
@@ -269,80 +333,42 @@ } | ||
*/ | ||
function renderExistingElement(insertionOptions) { | ||
const childNodes = insertionOptions.parentElement.childNodes; | ||
const parentState = getForgoState(insertionOptions.parentElement); | ||
function renderExistingElement(insertAt, childNodes, insertionOptions) { | ||
var _a; | ||
// Get rid of unwanted nodes. | ||
markNodesForUnloading(childNodes, insertionOptions.currentNodeIndex, insertAt); | ||
const targetElement = childNodes[insertionOptions.currentNodeIndex]; | ||
pendingAttachStates.forEach((pendingAttachState, i) => { | ||
var _a; | ||
if (pendingAttachState.key !== undefined) { | ||
const key = deriveComponentKey(pendingAttachState.key, i); | ||
const nodesForKey = (_a = parentState.lookups.newlyAddedKeyedComponentNodes.get(key)) !== null && _a !== void 0 ? _a : []; | ||
nodesForKey.push(targetElement); | ||
parentState.lookups.newlyAddedKeyedComponentNodes.set(key, nodesForKey); | ||
} | ||
}); | ||
if (forgoElement.key !== undefined) { | ||
parentState.lookups.newlyAddedKeyedElementNodes.set(forgoElement.key, targetElement); | ||
} | ||
syncAttrsAndState(forgoElement, targetElement, insertionOptions.currentNodeIndex, false, pendingAttachStates); | ||
const oldComponentState = (_a = getForgoState(targetElement)) === null || _a === void 0 ? void 0 : _a.components; | ||
syncAttrsAndState(forgoElement, targetElement, false, pendingAttachStates); | ||
renderChildNodes(targetElement); | ||
unloadMarkedNodes(targetElement, pendingAttachStates); | ||
unmountComponents(pendingAttachStates, oldComponentState); | ||
return { | ||
nodes: [targetElement], | ||
pendingMounts: [ | ||
() => mountComponents(pendingAttachStates, oldComponentState), | ||
], | ||
}; | ||
} | ||
function addElement(parentElement, position) { | ||
function addElement(parentElement, oldNode) { | ||
const newElement = createElement(forgoElement, parentElement); | ||
if (parentElement) { | ||
parentElement.insertBefore(newElement, oldNode); | ||
} | ||
if (forgoElement.props.ref) { | ||
forgoElement.props.ref.value = newElement; | ||
} | ||
const oldNode = position !== undefined | ||
? parentElement.childNodes[position] | ||
: null; | ||
if (parentElement) { | ||
const parentState = getForgoState(parentElement); | ||
pendingAttachStates.forEach((pendingAttachState, i) => { | ||
if (pendingAttachState.key !== undefined) { | ||
const key = deriveComponentKey(pendingAttachState.key, i); | ||
parentState.lookups.newlyAddedKeyedComponentNodes.set(key, [ | ||
newElement, | ||
]); | ||
} | ||
}); | ||
if (forgoElement.key !== undefined) { | ||
parentState.lookups.newlyAddedKeyedElementNodes.set(forgoElement.key, newElement); | ||
} | ||
} | ||
if (parentElement) { | ||
parentElement.insertBefore(newElement, oldNode !== null && oldNode !== void 0 ? oldNode : null); | ||
syncAttrsAndState(forgoElement, newElement, insertionOptions.type === "search" | ||
? insertionOptions.currentNodeIndex | ||
: findNodeIndex(parentElement.childNodes, newElement, 0), true, pendingAttachStates); | ||
} | ||
else { | ||
syncAttrsAndState(forgoElement, newElement, insertionOptions.type === "search" | ||
? insertionOptions.currentNodeIndex | ||
: -1, true, pendingAttachStates); | ||
} | ||
syncAttrsAndState(forgoElement, newElement, true, pendingAttachStates); | ||
renderChildNodes(newElement); | ||
return { nodes: [newElement] }; | ||
unmountComponents(pendingAttachStates, undefined); | ||
return { | ||
nodes: [newElement], | ||
pendingMounts: [() => mountComponents(pendingAttachStates, undefined)], | ||
}; | ||
} | ||
} | ||
function initKeyLookupLoop(state) { | ||
state.lookups.renderCount++; | ||
} | ||
function finalizeKeyLookups(state) { | ||
state.lookups.renderCount--; | ||
if (state.lookups.renderCount === 0) { | ||
state.lookups.keyedComponentNodes = | ||
state.lookups.newlyAddedKeyedComponentNodes; | ||
state.lookups.keyedElementNodes = | ||
state.lookups.newlyAddedKeyedElementNodes; | ||
state.lookups.newlyAddedKeyedComponentNodes = new Map(); | ||
state.lookups.newlyAddedKeyedElementNodes = new Map(); | ||
state.lookups.deletedKeyedComponentNodes = new Map(); | ||
state.lookups.deletedKeyedElementNodes = new Map(); | ||
state.lookups.deletedUnkeyedNodes = []; | ||
} | ||
} | ||
function renderComponent(forgoComponent, insertionOptions, pendingAttachStates, mountOnPreExistingDOM | ||
// boundary: ForgoComponent<object> | undefined | ||
/* | ||
Render a Component. | ||
Such as <MySideBar size="large" /> | ||
*/ | ||
function renderComponent(forgoElement, insertionOptions, pendingAttachStates, mountOnPreExistingDOM | ||
// boundary: ForgoComponent<any> | undefined | ||
) { | ||
@@ -353,7 +379,9 @@ const componentIndex = pendingAttachStates.length; | ||
insertionOptions.type !== "detached" && | ||
// We have to find a node to replace. | ||
insertionOptions.length && | ||
!mountOnPreExistingDOM) { | ||
const childNodes = insertionOptions.parentElement.childNodes; | ||
const found = findReplacementCandidateForComponent(forgoComponent, insertionOptions, pendingAttachStates.length); | ||
if (found) { | ||
return renderExistingComponent(childNodes, insertionOptions); | ||
const searchResult = findReplacementCandidateForComponent(forgoElement, insertionOptions.parentElement, insertionOptions.currentNodeIndex, insertionOptions.length, pendingAttachStates.length); | ||
if (searchResult.found) { | ||
return renderExistingComponent(searchResult.index, childNodes, insertionOptions); | ||
} | ||
@@ -364,13 +392,14 @@ } | ||
return addComponent(); | ||
function renderExistingComponent(childNodes, insertionOptions) { | ||
const targetNode = childNodes[insertionOptions.currentNodeIndex]; | ||
const state = getForgoState(targetNode); | ||
function renderExistingComponent(insertAt, childNodes, insertionOptions) { | ||
const targetNode = childNodes[insertAt]; | ||
const state = getExistingForgoState(targetNode); | ||
const componentState = state.components[componentIndex]; | ||
if (lifecycleEmitters.shouldUpdate(componentState.component, forgoComponent.props, componentState.props)) { | ||
// Get rid of unwanted nodes. | ||
markNodesForUnloading(childNodes, insertionOptions.currentNodeIndex, insertAt); | ||
if (lifecycleEmitters.shouldUpdate(componentState.component, forgoElement.props, componentState.props)) { | ||
// Since we have compatible state already stored, | ||
// we'll push the savedComponentState into pending states for later attachment. | ||
const updatedComponentState = Object.assign(Object.assign({}, componentState), { props: forgoComponent.props }); | ||
const updatedComponentState = Object.assign(Object.assign({}, componentState), { props: forgoElement.props }); | ||
// Get a new element by calling render on existing component. | ||
const newForgoNode = updatedComponentState.component.__internal.registeredMethods.render(forgoComponent.props, updatedComponentState.component); | ||
const componentIndex = pendingAttachStates.length; | ||
const newForgoNode = updatedComponentState.component.__internal.registeredMethods.render(forgoElement.props, updatedComponentState.component); | ||
const statesToAttach = pendingAttachStates.concat(updatedComponentState); | ||
@@ -382,3 +411,3 @@ const previousNode = componentState.component.__internal.element.node; | ||
: undefined; | ||
const renderResult = withErrorBoundary(forgoComponent.props, statesToAttach, boundary, () => { | ||
const renderResult = withErrorBoundary(forgoElement.props, statesToAttach, boundary, () => { | ||
// Create new node insertion options. | ||
@@ -391,5 +420,5 @@ const newInsertionOptions = { | ||
}; | ||
return renderComponentAndRemoveStaleNodes(newForgoNode, newInsertionOptions, statesToAttach, componentIndex, updatedComponentState.nodes.length, mountOnPreExistingDOM); | ||
return renderComponentAndRemoveStaleNodes(newForgoNode, newInsertionOptions, statesToAttach, updatedComponentState, mountOnPreExistingDOM); | ||
}); | ||
lifecycleEmitters.afterRender(updatedComponentState.component, forgoComponent.props, previousNode); | ||
lifecycleEmitters.afterRender(updatedComponentState.component, forgoElement.props, previousNode); | ||
return renderResult; | ||
@@ -399,4 +428,6 @@ } | ||
else { | ||
let indexOfNode = findNodeIndex(insertionOptions.parentElement.childNodes, componentState.component.__internal.element.node); | ||
return { | ||
nodes: componentState.nodes, | ||
nodes: sliceNodes(insertionOptions.parentElement.childNodes, indexOfNode, indexOfNode + componentState.nodes.length), | ||
pendingMounts: [], | ||
}; | ||
@@ -406,4 +437,4 @@ } | ||
function addComponent() { | ||
const ctor = forgoComponent.type; | ||
const component = assertIsComponent(ctor, ctor(forgoComponent.props), env.window.FORGO_NO_LEGACY_WARN !== true); | ||
const ctor = forgoElement.type; | ||
const component = assertIsComponent(ctor, ctor(forgoElement.props), env.window.FORGO_NO_LEGACY_WARN !== true); | ||
component.__internal.element.componentIndex = componentIndex; | ||
@@ -416,14 +447,13 @@ const boundary = component.__internal.registeredMethods.error | ||
const newComponentState = { | ||
key: forgoComponent.key, | ||
key: forgoElement.key, | ||
ctor, | ||
component, | ||
props: forgoComponent.props, | ||
props: forgoElement.props, | ||
nodes: [], | ||
isMounted: false, | ||
}; | ||
const indexOfNewComponentState = pendingAttachStates.length; | ||
const statesToAttach = pendingAttachStates.concat(newComponentState); | ||
return withErrorBoundary(forgoComponent.props, statesToAttach, boundary, () => { | ||
return withErrorBoundary(forgoElement.props, statesToAttach, boundary, () => { | ||
// Create an element by rendering the component | ||
const newForgoElement = component.__internal.registeredMethods.render(forgoComponent.props, component); | ||
const newForgoElement = component.__internal.registeredMethods.render(forgoElement.props, component); | ||
// Create new node insertion options. | ||
@@ -440,12 +470,10 @@ const newInsertionOptions = insertionOptions.type === "detached" | ||
const renderResult = internalRender(newForgoElement, newInsertionOptions, statesToAttach, mountOnPreExistingDOM); | ||
const nodeAttachedState = getForgoState(renderResult.nodes[0]); | ||
const componentStateAttached = nodeAttachedState.components[indexOfNewComponentState]; | ||
componentStateAttached.nodes = renderResult.nodes; | ||
setNodeInfo(componentStateAttached.component.__internal.element, renderResult.nodes[0], insertionOptions.type !== "detached" | ||
? insertionOptions.currentNodeIndex | ||
: -1); | ||
lifecycleEmitters.mount(newComponentState.component, forgoComponent.props); | ||
// In case we rendered an array, set the node to the first node. | ||
// We do this because args.element.node would be set to the last node otherwise. | ||
newComponentState.nodes = renderResult.nodes; | ||
newComponentState.component.__internal.element.node = | ||
renderResult.nodes[0]; | ||
// No previousNode since new component. So just args and not | ||
// afterRenderArgs. | ||
lifecycleEmitters.afterRender(component, forgoComponent.props, undefined); | ||
lifecycleEmitters.afterRender(component, forgoElement.props, undefined); | ||
return renderResult; | ||
@@ -469,12 +497,6 @@ }); | ||
} | ||
function renderComponentAndRemoveStaleNodes(forgoNode, insertionOptions, statesToAttach, componentIndex, previousNodeCount, mountOnPreExistingDOM) { | ||
function renderComponentAndRemoveStaleNodes(forgoNode, insertionOptions, statesToAttach, componentState, mountOnPreExistingDOM) { | ||
const totalNodesBeforeRender = insertionOptions.parentElement.childNodes.length; | ||
const componentState = statesToAttach.slice(-1)[0]; | ||
const previousNode = componentState.component.__internal.element.node; | ||
// Pass it on for rendering... | ||
const renderResult = internalRender(forgoNode, insertionOptions, statesToAttach, mountOnPreExistingDOM); | ||
const newNode = componentState.component.__internal.element.node; | ||
if (previousNode !== newNode) { | ||
lifecycleEmitters.remount(componentState.component, componentState.props); | ||
} | ||
const totalNodesAfterRender = insertionOptions.parentElement.childNodes.length; | ||
@@ -489,15 +511,28 @@ const numNodesReused = totalNodesBeforeRender + | ||
const deleteFromIndex = insertionOptions.currentNodeIndex + renderResult.nodes.length; | ||
markNodesForUnloading(insertionOptions.parentElement.childNodes, deleteFromIndex, deleteFromIndex + previousNodeCount - numNodesReused); | ||
// In case we rendered an array, set the node to the first node. We do this | ||
// because args.element.node would be set to the last node otherwise. | ||
// There's also a chance that renderResult might have no nodes. For example, | ||
// if render returned an empty fragment. | ||
if (renderResult.nodes.length) { | ||
const nodeAttachedState = getForgoState(renderResult.nodes[0]); | ||
const componentStateAttached = nodeAttachedState.components[componentIndex]; | ||
componentStateAttached.nodes = renderResult.nodes; | ||
setNodeInfo(componentStateAttached.component.__internal.element, renderResult.nodes[0], insertionOptions.currentNodeIndex); | ||
const deletedNodes = markNodesForUnloading(insertionOptions.parentElement.childNodes, deleteFromIndex, deleteFromIndex + componentState.nodes.length - numNodesReused); | ||
/* | ||
* transferredState is the state that's already been remounted on a different node. | ||
* Components in transferredState should not be unmounted, since this is already | ||
* being tracked on a different node. Hence transferredState needs to be removed | ||
* from deletedNodes. | ||
*/ | ||
const transferredState = renderResult.nodes.length > 0 ? statesToAttach : []; | ||
// Patch state in deletedNodes to exclude what's been already transferred. | ||
for (const deletedNode of deletedNodes) { | ||
const state = getForgoState(deletedNode); | ||
if (state) { | ||
const indexOfFirstIncompatibleState = findIndexOfFirstIncompatibleState(transferredState, state.components); | ||
state.components = state.components.slice(indexOfFirstIncompatibleState); | ||
} | ||
} | ||
// In case we rendered an array, set the node to the first node. | ||
// We do this because args.element.node would be set to the last node otherwise. | ||
componentState.nodes = renderResult.nodes; | ||
componentState.component.__internal.element.node = renderResult.nodes[0]; | ||
return renderResult; | ||
} | ||
/* | ||
Render an array of components. | ||
Called when a Component returns an array (or fragment) in its render method. | ||
*/ | ||
function renderArray(forgoNodes, insertionOptions, pendingAttachStates, mountOnPreExistingDOM) { | ||
@@ -509,7 +544,5 @@ const flattenedNodes = flatten(forgoNodes); | ||
else { | ||
const renderResults = { nodes: [] }; | ||
const renderResults = { nodes: [], pendingMounts: [] }; | ||
let currentNodeIndex = insertionOptions.currentNodeIndex; | ||
let numNodes = insertionOptions.length; | ||
const parentState = getForgoState(insertionOptions.parentElement); | ||
initKeyLookupLoop(parentState); | ||
for (const forgoNode of flattenedNodes) { | ||
@@ -520,2 +553,3 @@ const totalNodesBeforeRender = insertionOptions.parentElement.childNodes.length; | ||
renderResults.nodes.push(...renderResult.nodes); | ||
renderResults.pendingMounts.push(...renderResult.pendingMounts); | ||
const totalNodesAfterRender = insertionOptions.parentElement.childNodes.length; | ||
@@ -528,3 +562,2 @@ const numNodesRemoved = totalNodesBeforeRender + | ||
} | ||
finalizeKeyLookups(parentState); | ||
return renderResults; | ||
@@ -535,5 +568,5 @@ } | ||
* This doesn't unmount components attached to these nodes, but moves the node | ||
* itself from the DOM to deletedXYXNodes under parentNode.lookups. 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. | ||
* 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. | ||
* | ||
@@ -543,189 +576,213 @@ * Nodes in between `from` and `to` (not inclusive of `to`) will be marked for | ||
* 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 removedNodes = []; | ||
if (to > from) { | ||
const parentElement = nodes[from].parentElement; | ||
const parentState = getForgoState(parentElement); | ||
for (let i = from; i < to; i++) { | ||
const node = nodes[from]; | ||
const justDeletedNodes = []; | ||
const nodesToRemove = sliceNodes(nodes, from, to); | ||
if (nodesToRemove.length) { | ||
const parentElement = nodesToRemove[0].parentElement; | ||
const deletedNodes = getDeletedNodes(parentElement); | ||
for (const node of nodesToRemove) { | ||
// If the consuming application has manually mucked with the DOM don't | ||
// remove things it added | ||
const state = getForgoState(node); | ||
// Remove the node from DOM | ||
if (!state) | ||
continue; | ||
node.remove(); | ||
// If the component is keyed, we have to remove the entry in key-map | ||
state.components.forEach((component, i) => { | ||
var _a; | ||
if (component.key !== undefined) { | ||
const key = deriveComponentKey(component.key, i); | ||
const nodesForKey = parentState.lookups.keyedComponentNodes.get(key); | ||
if (nodesForKey !== undefined) { | ||
const updatedNodesForKey = nodesForKey.filter((x) => x !== node); | ||
if (updatedNodesForKey.length) { | ||
parentState.lookups.keyedComponentNodes.set(key, updatedNodesForKey); | ||
} | ||
else { | ||
parentState.lookups.keyedComponentNodes.delete(key); | ||
} | ||
} | ||
const deletedNodesForKey = (_a = parentState.lookups.deletedKeyedComponentNodes.get(key)) !== null && _a !== void 0 ? _a : []; | ||
deletedNodesForKey.push(node); | ||
parentState.lookups.deletedKeyedComponentNodes.set(key, deletedNodesForKey); | ||
} | ||
}); | ||
if (state.key !== undefined) { | ||
parentState.lookups.keyedComponentNodes.delete(state.key); | ||
parentState.lookups.deletedKeyedComponentNodes.set(state.key, [node]); | ||
} | ||
else { | ||
parentState.lookups.deletedUnkeyedNodes.push({ node }); | ||
} | ||
removedNodes.push(node); | ||
justDeletedNodes.push(node); | ||
deletedNodes.push({ node }); | ||
} | ||
} | ||
return removedNodes; | ||
return justDeletedNodes; | ||
} | ||
/* | ||
* Unmount components from nodes. If a componentState is attached to the node | ||
* that is about to be unloaded, then we should unmount the component. | ||
*/ | ||
function unloadMarkedNodes(parentElement) { | ||
function unloadNode(node) { | ||
Unmount components from nodes. | ||
We unmount only after first incompatible state, since compatible states | ||
will be reattached to new candidate node. | ||
*/ | ||
function unloadMarkedNodes(parentElement, pendingAttachStates) { | ||
const deletedNodes = getDeletedNodes(parentElement); | ||
for (const { node } of deletedNodes) { | ||
const state = getForgoState(node); | ||
state.deleted = true; | ||
for (const componentState of state.components) { | ||
if (componentState.component.__internal.element.node === node) { | ||
if (!componentState.component.__internal.unmounted) { | ||
lifecycleEmitters.unmount(componentState.component, componentState.props); | ||
} | ||
if (state) { | ||
state.deleted = true; | ||
const oldComponentStates = state.components; | ||
unmountComponents(pendingAttachStates, oldComponentStates); | ||
} | ||
} | ||
clearDeletedNodes(parentElement); | ||
} | ||
/* | ||
When states are attached to a new node or when states are reattached, | ||
some of the old component states need to go away. The corresponding components | ||
will need to be unmounted. | ||
While rendering, the component gets reused if the ctor is the same. If the | ||
ctor is different, the component is discarded. And hence needs to be unmounted. | ||
So we check the ctor type in old and new. | ||
*/ | ||
function findIndexOfFirstIncompatibleState(newStates, oldStates) { | ||
let i = 0; | ||
for (const newState of newStates) { | ||
if (oldStates.length > i) { | ||
const oldState = oldStates[i]; | ||
if (oldState.component !== newState.component) { | ||
break; | ||
} | ||
i++; | ||
} | ||
else { | ||
break; | ||
} | ||
} | ||
const parentState = getForgoState(parentElement); | ||
for (const nodeList of parentState.lookups.deletedKeyedComponentNodes.values()) { | ||
for (const node of nodeList) { | ||
if (node.isConnected) { | ||
unloadNode(node); | ||
return i; | ||
} | ||
/** | ||
* Unmount components above an index. This is going to be passed a stale | ||
* state[]. | ||
* | ||
* The `unmount` lifecycle event will be called. | ||
*/ | ||
function unmountComponents(pendingAttachStates, oldComponentStates) { | ||
if (!oldComponentStates) | ||
return; | ||
// If the parent has already unmounted, we can skip checks on children. | ||
let parentHasUnmounted = false; | ||
const indexOfFirstIncompatibleState = findIndexOfFirstIncompatibleState(pendingAttachStates, oldComponentStates); | ||
for (let i = indexOfFirstIncompatibleState; i < oldComponentStates.length; i++) { | ||
const state = oldComponentStates[i]; | ||
const component = state.component; | ||
// Render if: | ||
// - parent has already unmounted | ||
// - OR for all nodes: | ||
// - node is disconnected | ||
// - OR node connected to a different component | ||
if (parentHasUnmounted || | ||
state.nodes.every((x) => { | ||
if (!x.isConnected) { | ||
return true; | ||
} | ||
else { | ||
const stateOnCurrentNode = getExistingForgoState(x); | ||
return (!stateOnCurrentNode.components[i] || | ||
stateOnCurrentNode.components[i].component !== state.component); | ||
} | ||
})) { | ||
if (!component.__internal.unmounted) { | ||
component.__internal.unmounted = true; | ||
lifecycleEmitters.unmount(component, state.props); | ||
} | ||
parentHasUnmounted = true; | ||
} | ||
} | ||
for (const { node } of parentState.lookups.deletedUnkeyedNodes) { | ||
unloadNode(node); | ||
} | ||
/** | ||
* Mount components above an index. This is going to be passed the new | ||
* state[]. | ||
*/ | ||
function mountComponents(pendingAttachStates, oldComponentStates) { | ||
const indexOfFirstIncompatibleState = oldComponentStates | ||
? findIndexOfFirstIncompatibleState(pendingAttachStates, oldComponentStates) | ||
: 0; | ||
for (let i = indexOfFirstIncompatibleState; i < pendingAttachStates.length; i++) { | ||
const state = pendingAttachStates[i]; | ||
// This function is called in every syncStateAndProps() call, so many of | ||
// the calls will be for already-mounted components. Only fire the mount | ||
// lifecycle events when appropriate. | ||
if (!state.isMounted) { | ||
state.isMounted = true; | ||
// Set this before calling the lifecycle handlers to fix #70 | ||
lifecycleEmitters.mount(state.component, state.props); | ||
} | ||
} | ||
// Clear deleted nodes | ||
parentState.lookups.deletedKeyedComponentNodes.clear(); | ||
parentState.lookups.deletedUnkeyedNodes = []; | ||
} | ||
function findReplacementCandidateForElement(forgoElement, insertionOptions, pendingAttachStates) { | ||
function isCompatibleElement(node, forgoElement, pendingAttachStates) { | ||
/** | ||
* 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) { | ||
const nodes = parentElement.childNodes; | ||
for (let i = searchFrom; i < searchFrom + length; i++) { | ||
const node = nodes[i]; | ||
if (nodeIsElement(node)) { | ||
const state = getForgoState(node); | ||
return (node.tagName.toLowerCase() === forgoElement.type && | ||
state.components.every((componentState, i) => pendingAttachStates[i] !== undefined && | ||
pendingAttachStates[i].component === componentState.component)); | ||
} | ||
else { | ||
return false; | ||
} | ||
} | ||
function findReplacementCandidateForKeyedElement(forgoElement, insertionOptions, pendingAttachStates) { | ||
const { parentElement, currentNodeIndex: searchFrom } = insertionOptions; | ||
// First let's check active nodes. | ||
const parentState = getForgoState(parentElement); | ||
// See if the node is in our key lookup | ||
const nodeFromKeyLookup = parentState.lookups.keyedElementNodes.get(forgoElement.key); | ||
if (nodeFromKeyLookup !== undefined) { | ||
if (isCompatibleElement(nodeFromKeyLookup, forgoElement, pendingAttachStates)) { | ||
// Let's insert the nodes at the corresponding position. | ||
const firstNodeInSearchList = parentElement.childNodes[searchFrom]; | ||
if (nodeFromKeyLookup !== firstNodeInSearchList) { | ||
parentElement.insertBefore(nodeFromKeyLookup, firstNodeInSearchList !== null && firstNodeInSearchList !== void 0 ? firstNodeInSearchList : null); | ||
} | ||
return true; | ||
const stateOnNode = getForgoState(node); | ||
// 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 }; | ||
} | ||
else { | ||
// Node is mismatched. No point in keeping it in key lookup. | ||
parentState.lookups.keyedComponentNodes.delete(forgoElement.key); | ||
return false; | ||
} | ||
} | ||
// Not found in active nodes. Check deleted nodes. | ||
else { | ||
const nodeFromKeyLookup = parentState.lookups.deletedKeyedElementNodes.get(forgoElement.key); | ||
if (nodeFromKeyLookup !== undefined) { | ||
const nodes = parentElement.childNodes; | ||
// Delete key from lookup since we're either going to resurrect the node or discard it. | ||
parentState.lookups.deletedKeyedComponentNodes.delete(forgoElement.key); | ||
if (isCompatibleElement(nodeFromKeyLookup, forgoElement, pendingAttachStates)) { | ||
// Let's insert the nodes at the corresponding position. | ||
const firstNodeInSearchList = nodes[searchFrom]; | ||
if (nodeFromKeyLookup !== firstNodeInSearchList) { | ||
parentElement.insertBefore(nodeFromKeyLookup, firstNodeInSearchList !== null && firstNodeInSearchList !== void 0 ? firstNodeInSearchList : null); | ||
} | ||
return true; | ||
// If the candidate has a key defined, | ||
// we don't match it with an unkeyed forgo element | ||
if (node.tagName.toLowerCase() === forgoElement.type && | ||
!(stateOnNode === null || stateOnNode === void 0 ? void 0 : stateOnNode.key)) { | ||
return { found: true, index: i }; | ||
} | ||
} | ||
return false; | ||
} | ||
} | ||
function findReplacementCandidateForUnKeyedElement(forgoElement, insertionOptions, pendingAttachStates) { | ||
var _a; | ||
const { parentElement, currentNodeIndex: searchFrom, length, } = insertionOptions; | ||
const nodes = parentElement.childNodes; | ||
for (let i = searchFrom; i < searchFrom + length; i++) { | ||
const node = nodes[i]; | ||
if (nodeIsElement(node)) { | ||
const state = getForgoState(node); | ||
// If the candidate has a key defined, we don't match it with | ||
// an unkeyed forgo element | ||
if (node.tagName.toLowerCase() === forgoElement.type && | ||
state.key === undefined && | ||
isCompatibleElement(node, forgoElement, pendingAttachStates)) { | ||
const elementAtSearchIndex = (_a = parentElement.childNodes[searchFrom]) !== null && _a !== void 0 ? _a : null; | ||
if (node !== elementAtSearchIndex) { | ||
parentElement.insertBefore(node, elementAtSearchIndex); | ||
} | ||
return true; | ||
// Let's check deleted nodes as well. | ||
if (forgoElement.key !== undefined) { | ||
const deletedNodes = getDeletedNodes(parentElement); | ||
for (let i = 0; i < deletedNodes.length; i++) { | ||
const { node } = deletedNodes[i]; | ||
const stateOnNode = getForgoState(node); | ||
if ((stateOnNode === null || stateOnNode === void 0 ? void 0 : stateOnNode.key) === forgoElement.key) { | ||
// Remove it from deletedNodes. | ||
deletedNodes.splice(i, 1); | ||
// Append it to the beginning of the node list. | ||
const firstNodeInSearchList = nodes[searchFrom]; | ||
if (firstNodeInSearchList) { | ||
parentElement.insertBefore(node, firstNodeInSearchList); | ||
} | ||
else { | ||
parentElement.appendChild(node); | ||
} | ||
return { found: true, index: searchFrom }; | ||
} | ||
} | ||
return false; | ||
} | ||
if (isKeyedElement(forgoElement)) { | ||
return findReplacementCandidateForKeyedElement(forgoElement, insertionOptions, pendingAttachStates); | ||
} | ||
else { | ||
return findReplacementCandidateForUnKeyedElement(forgoElement, insertionOptions, pendingAttachStates); | ||
} | ||
return { found: false }; | ||
} | ||
function findReplacementCandidateForComponent(forgoComponent, insertionOptions, componentIndex) { | ||
function findReplacementCandidateForKeyedComponent(forgoComponent, insertionOptions, componentIndex) { | ||
const { parentElement, currentNodeIndex: searchFrom } = insertionOptions; | ||
const key = deriveComponentKey(forgoComponent.key, componentIndex); | ||
// If forgo element has a key, we gotta find it in the childNodeMap (under active and deleted). | ||
const parentState = getForgoState(parentElement); | ||
// Check active nodes first | ||
const nodesForKey = parentState.lookups.keyedComponentNodes.get(key); | ||
if (nodesForKey !== undefined) { | ||
// Let's insert the nodes at the corresponding position. | ||
const elementAtIndex = parentElement.childNodes[searchFrom]; | ||
for (const node of nodesForKey) { | ||
if (node !== elementAtIndex) { | ||
parentElement.insertBefore(node, elementAtIndex !== null && elementAtIndex !== void 0 ? elementAtIndex : null); | ||
/** | ||
* 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) { | ||
const nodes = parentElement.childNodes; | ||
for (let i = searchFrom; i < searchFrom + length; i++) { | ||
const node = nodes[i]; | ||
const stateOnNode = getForgoState(node); | ||
if (stateOnNode && stateOnNode.components.length > componentIndex) { | ||
if (forgoElement.key !== undefined) { | ||
if (stateOnNode.components[componentIndex].ctor === forgoElement.type && | ||
stateOnNode.components[componentIndex].key === forgoElement.key) { | ||
return { found: true, index: i }; | ||
} | ||
} | ||
return true; | ||
else { | ||
if (stateOnNode.components[componentIndex].ctor === forgoElement.type) { | ||
return { found: true, index: i }; | ||
} | ||
} | ||
} | ||
// Not found in active nodes. Check deleted nodes. | ||
else { | ||
const matchingNodes = parentState.lookups.deletedKeyedComponentNodes.get(key); | ||
if (matchingNodes !== undefined) { | ||
// Delete key from lookup since we're either going to resurrect these nodes | ||
parentState.lookups.deletedKeyedComponentNodes.delete(key); | ||
// Append it to the beginning of the node list. | ||
for (const node of matchingNodes) { | ||
const firstNodeInSearchList = parentElement.childNodes[searchFrom]; | ||
if (node !== firstNodeInSearchList) { | ||
parentElement.insertBefore(node, firstNodeInSearchList !== null && firstNodeInSearchList !== void 0 ? firstNodeInSearchList : null); | ||
} | ||
} | ||
} | ||
// Check if a keyed component is mounted on this node. | ||
function nodeBelongsToKeyedComponent(node, forgoElement, componentIndex) { | ||
const stateOnNode = getForgoState(node); | ||
if (stateOnNode && stateOnNode.components.length > componentIndex) { | ||
if (stateOnNode.components[componentIndex].ctor === forgoElement.type && | ||
stateOnNode.components[componentIndex].key === forgoElement.key) { | ||
return true; | ||
@@ -736,27 +793,36 @@ } | ||
} | ||
function findReplacementCandidateForUnkeyedComponent(forgoComponent, insertionOptions, componentIndex) { | ||
var _a; | ||
const { parentElement, currentNodeIndex: searchFrom, length, } = insertionOptions; | ||
const nodes = parentElement.childNodes; | ||
for (let i = searchFrom; i < searchFrom + length; i++) { | ||
const node = nodes[i]; | ||
const state = getForgoState(node); | ||
if (state !== undefined && state.components.length > componentIndex) { | ||
if (state.components[componentIndex].ctor === forgoComponent.type) { | ||
const elementAtSearchIndex = (_a = parentElement.childNodes[searchFrom]) !== null && _a !== void 0 ? _a : null; | ||
if (node !== elementAtSearchIndex) { | ||
parentElement.insertBefore(node, elementAtSearchIndex); | ||
// Let's check deleted nodes as well. | ||
if (forgoElement.key !== undefined) { | ||
const deletedNodes = getDeletedNodes(parentElement); | ||
for (let i = 0; i < deletedNodes.length; i++) { | ||
const { node: deletedNode } = deletedNodes[i]; | ||
if (nodeBelongsToKeyedComponent(deletedNode, forgoElement, componentIndex)) { | ||
const nodesToResurrect = [deletedNode]; | ||
// Found a match! | ||
// Collect all consecutive matching nodes. | ||
for (let j = i + 1; j < deletedNodes.length; j++) { | ||
const { node: subsequentNode } = deletedNodes[j]; | ||
if (nodeBelongsToKeyedComponent(subsequentNode, forgoElement, componentIndex)) { | ||
nodesToResurrect.push(subsequentNode); | ||
} | ||
return true; | ||
} | ||
// Remove them from deletedNodes. | ||
deletedNodes.splice(i, nodesToResurrect.length); | ||
// Append resurrected nodes to the beginning of the node list. | ||
let insertBeforeNode = nodes[searchFrom]; | ||
if (insertBeforeNode) { | ||
for (const node of nodesToResurrect) { | ||
parentElement.insertBefore(node, insertBeforeNode); | ||
} | ||
} | ||
else { | ||
for (const node of nodesToResurrect) { | ||
parentElement.appendChild(node); | ||
} | ||
} | ||
return { found: true, index: searchFrom }; | ||
} | ||
} | ||
return false; | ||
} | ||
if (isKeyedElement(forgoComponent)) { | ||
return findReplacementCandidateForKeyedComponent(forgoComponent, insertionOptions, componentIndex); | ||
} | ||
else { | ||
return findReplacementCandidateForUnkeyedComponent(forgoComponent, insertionOptions, componentIndex); | ||
} | ||
return { found: false }; | ||
} | ||
@@ -767,3 +833,3 @@ /** | ||
*/ | ||
function syncAttrsAndState(forgoNode, node, nodeIndex, isNewNode, pendingAttachStates) { | ||
function syncAttrsAndState(forgoNode, node, isNewNode, pendingAttachStates) { | ||
var _a; | ||
@@ -774,10 +840,9 @@ // We have to inject node into the args object. | ||
if (pendingAttachStates.length > 0) { | ||
setNodeInfo(pendingAttachStates[pendingAttachStates.length - 1].component.__internal | ||
.element, node, nodeIndex); | ||
pendingAttachStates[pendingAttachStates.length - 1].component.__internal.element.node = node; | ||
} | ||
if (isForgoElement(forgoNode)) { | ||
const existingState = getForgoState(node); | ||
const currentState = getForgoState(node); | ||
// Remove props which don't exist | ||
if (existingState !== undefined && existingState.props) { | ||
for (const key in existingState.props) { | ||
if (currentState && currentState.props) { | ||
for (const key in currentState.props) { | ||
if (!(key in forgoNode.props)) { | ||
@@ -828,3 +893,3 @@ if (key !== "children" && key !== "xmlns") { | ||
// necessary. See issue #32. | ||
if (((_a = existingState === null || existingState === void 0 ? void 0 : existingState.props) === null || _a === void 0 ? void 0 : _a[key]) !== value) { | ||
if (((_a = currentState === null || currentState === void 0 ? void 0 : currentState.props) === null || _a === void 0 ? void 0 : _a[key]) !== value) { | ||
if (key !== "children" && key !== "xmlns") { | ||
@@ -839,5 +904,5 @@ if (node.nodeType === TEXT_NODE_TYPE || | ||
// If they're the same, skip the expensive styleToString() call. | ||
if (existingState === undefined || | ||
existingState.style === undefined || | ||
existingState.style !== forgoNode.props.style) { | ||
if (currentState === undefined || | ||
currentState.style === undefined || | ||
currentState.style !== forgoNode.props.style) { | ||
const stringOfCSS = styleToString(forgoNode.props.style); | ||
@@ -872,3 +937,7 @@ if (node.style.cssText !== stringOfCSS) { | ||
// Now attach the internal forgo state. | ||
const state = Object.assign(Object.assign({}, existingState), { key: forgoNode.key, props: forgoNode.props, components: pendingAttachStates }); | ||
const state = { | ||
key: forgoNode.key, | ||
props: forgoNode.props, | ||
components: pendingAttachStates, | ||
}; | ||
setForgoState(node, state); | ||
@@ -880,12 +949,2 @@ } | ||
components: pendingAttachStates, | ||
lookups: { | ||
deletedKeyedComponentNodes: new Map(), | ||
deletedUnkeyedNodes: [], | ||
keyedComponentNodes: new Map(), | ||
newlyAddedKeyedComponentNodes: new Map(), | ||
deletedKeyedElementNodes: new Map(), | ||
newlyAddedKeyedElementNodes: new Map(), | ||
keyedElementNodes: new Map(), | ||
renderCount: 0, | ||
}, | ||
}; | ||
@@ -899,4 +958,4 @@ setForgoState(node, state); | ||
function mount(forgoNode, container) { | ||
const parentElement = (isString(container) ? env.document.querySelector(container) : container); | ||
if (parentElement == undefined) { | ||
let parentElement = (isString(container) ? env.document.querySelector(container) : container); | ||
if (!parentElement) { | ||
throw new Error(`The mount() function was called on a non-element (${typeof container === "string" ? container : container === null || container === void 0 ? void 0 : container.tagName}).`); | ||
@@ -925,6 +984,4 @@ } | ||
function unmount(container) { | ||
const parentElement = isString(container) | ||
? env.document.querySelector(container) | ||
: container; | ||
if (parentElement === null) { | ||
let parentElement = (isString(container) ? env.document.querySelector(container) : container); | ||
if (!parentElement) { | ||
throw new Error(`The unmount() function was called on a non-element (${typeof container === "string" ? container : container === null || container === void 0 ? void 0 : container.tagName}).`); | ||
@@ -936,3 +993,3 @@ } | ||
markNodesForUnloading(parentElement.childNodes, 0, parentElement.childNodes.length); | ||
unloadMarkedNodes(parentElement); | ||
unloadMarkedNodes(parentElement, []); | ||
} | ||
@@ -963,16 +1020,15 @@ /* | ||
function rerender(element, props) { | ||
if (element === undefined || element.node === undefined) { | ||
if (!(element === null || element === void 0 ? void 0 : element.node)) { | ||
throw new Error(`Missing node information in rerender() argument.`); | ||
} | ||
if (element.node.parentElement !== null && element.nodeIndex === -1) { | ||
element.nodeIndex = findNodeIndex(element.node.parentElement.childNodes, element.node, 0); | ||
} | ||
const parentElement = element.node.parentElement; | ||
if (!isNullOrUndefined(parentElement)) { | ||
const state = getForgoState(element.node); | ||
if (parentElement !== null) { | ||
const state = getExistingForgoState(element.node); | ||
const originalComponentState = state.components[element.componentIndex]; | ||
const effectiveProps = props !== null && props !== void 0 ? props : originalComponentState.props; | ||
if (!lifecycleEmitters.shouldUpdate(originalComponentState.component, effectiveProps, originalComponentState.props)) { | ||
let indexOfNode = findNodeIndex(parentElement.childNodes, element.node); | ||
return { | ||
nodes: sliceNodes(parentElement.childNodes, element.nodeIndex, element.nodeIndex + originalComponentState.nodes.length), | ||
nodes: sliceNodes(parentElement.childNodes, indexOfNode, indexOfNode + originalComponentState.nodes.length), | ||
pendingMounts: [], | ||
}; | ||
@@ -982,16 +1038,16 @@ } | ||
const parentStates = state.components.slice(0, element.componentIndex); | ||
const componentIndex = parentStates.length; | ||
const statesToAttach = parentStates.concat(componentStateWithUpdatedProps); | ||
const previousNode = originalComponentState.component.__internal.element.node; | ||
const forgoNode = originalComponentState.component.__internal.registeredMethods.render(effectiveProps, originalComponentState.component); | ||
let nodeIndex = findNodeIndex(parentElement.childNodes, element.node); | ||
const insertionOptions = { | ||
type: "search", | ||
currentNodeIndex: element.nodeIndex, | ||
currentNodeIndex: nodeIndex, | ||
length: originalComponentState.nodes.length, | ||
parentElement, | ||
}; | ||
const renderResult = renderComponentAndRemoveStaleNodes(forgoNode, insertionOptions, statesToAttach, componentIndex, originalComponentState.nodes.length, false); | ||
const renderResult = renderComponentAndRemoveStaleNodes(forgoNode, insertionOptions, statesToAttach, componentStateWithUpdatedProps, false); | ||
// We have to propagate node changes up the component Tree. | ||
// Reason 1: | ||
// Imagine a Parent rendering Child1 & Child2 | ||
// Imaging Parent rendering Child1 & Child2 | ||
// Child1 renders [div1, div2], and Child2 renders [div3, div4]. | ||
@@ -1017,6 +1073,11 @@ // When Child1's rerender is called, it might return [p1] instead of [div1, div2] | ||
// The root node might have changed, so fix it up just in case. | ||
setNodeInfo(parentState.component.__internal.element, parentState.nodes[0], indexOfOriginalRootNode); | ||
parentState.component.__internal.element.node = parentState.nodes[0]; | ||
} | ||
} | ||
unloadMarkedNodes(parentElement); | ||
// Unload marked nodes. | ||
unloadMarkedNodes(parentElement, renderResult.nodes.length > 0 ? statesToAttach : []); | ||
// Unmount rendered component itself if all nodes are gone. | ||
// if (renderResult.nodes.length === 0) { | ||
// unmountComponents([newComponentState], 0); | ||
// } | ||
// Run afterRender() if defined. | ||
@@ -1027,15 +1088,12 @@ lifecycleEmitters.afterRender(originalComponentState.component, effectiveProps, previousNode); | ||
else { | ||
return { nodes: [] }; | ||
return { nodes: [], pendingMounts: [] }; | ||
} | ||
} | ||
function createElement(forgoElement, element) { | ||
const namespaceURI = !isNullOrUndefined(forgoElement.props.xmlns) | ||
? forgoElement.props.xmlns | ||
: forgoElement.type === "svg" | ||
? SVG_NAMESPACE | ||
: element !== undefined | ||
? element.namespaceURI | ||
: null; | ||
if (forgoElement.props.is !== undefined) { | ||
return namespaceURI !== null | ||
function createElement(forgoElement, parentElement) { | ||
var _a; | ||
const namespaceURI = ((_a = forgoElement.props.xmlns) !== null && _a !== void 0 ? _a : forgoElement.type === "svg") | ||
? SVG_NAMESPACE | ||
: parentElement && parentElement.namespaceURI; | ||
if (forgoElement.props.is) { | ||
return namespaceURI | ||
? env.document.createElementNS(namespaceURI, forgoElement.type, { | ||
@@ -1049,3 +1107,3 @@ is: forgoElement.props.is, | ||
else { | ||
return namespaceURI !== null | ||
return namespaceURI | ||
? env.document.createElementNS(namespaceURI, forgoElement.type) | ||
@@ -1062,3 +1120,3 @@ : env.document.createElement(forgoElement.type); | ||
} | ||
const windowObject = globalThis !== undefined ? globalThis : window; | ||
const windowObject = globalThis ? globalThis : window; | ||
let forgoInstance = createForgoInstance({ | ||
@@ -1101,3 +1159,4 @@ window: windowObject, | ||
? itemOrItems.props.children | ||
: !isNullOrUndefined(itemOrItems.props.children) | ||
: itemOrItems.props.children !== undefined && | ||
itemOrItems.props.children !== null | ||
? [itemOrItems.props.children] | ||
@@ -1129,3 +1188,4 @@ : [] | ||
function isForgoElement(forgoNode) { | ||
return (!isNullOrUndefined(forgoNode) && | ||
return (forgoNode !== undefined && | ||
forgoNode !== null && | ||
forgoNode.__is_forgo_element__ === true); | ||
@@ -1137,3 +1197,3 @@ } | ||
function isForgoFragment(node) { | ||
return !isNullOrUndefined(node) && node.type === Fragment; | ||
return node !== undefined && node !== null && node.type === Fragment; | ||
} | ||
@@ -1144,20 +1204,16 @@ /* | ||
export function getForgoState(node) { | ||
if (node.__forgo === undefined) { | ||
node.__forgo = { | ||
components: [], | ||
lookups: { | ||
deletedKeyedComponentNodes: new Map(), | ||
deletedUnkeyedNodes: [], | ||
keyedComponentNodes: new Map(), | ||
newlyAddedKeyedComponentNodes: new Map(), | ||
deletedKeyedElementNodes: new Map(), | ||
keyedElementNodes: new Map(), | ||
newlyAddedKeyedElementNodes: new Map(), | ||
renderCount: 0, | ||
}, | ||
}; | ||
} | ||
return node.__forgo; | ||
} | ||
/* | ||
Same as above, but throws if undefined. (Caller must make sure.) | ||
*/ | ||
function getExistingForgoState(node) { | ||
if (node.__forgo) { | ||
return node.__forgo; | ||
} | ||
else { | ||
throw new Error("Missing forgo state on node."); | ||
} | ||
} | ||
/* | ||
Sets the state (NodeAttachedState) on an element. | ||
@@ -1168,2 +1224,17 @@ */ | ||
} | ||
/* | ||
We maintain a list of deleted childNodes on an element. | ||
In case we need to resurrect it - on account of a subsequent out-of-order key referring that node. | ||
*/ | ||
function getDeletedNodes(element) { | ||
if (!element.__forgo_deletedNodes) { | ||
element.__forgo_deletedNodes = []; | ||
} | ||
return element.__forgo_deletedNodes; | ||
} | ||
function clearDeletedNodes(element) { | ||
if (element.__forgo_deletedNodes) { | ||
element.__forgo_deletedNodes = []; | ||
} | ||
} | ||
// We export this so forgo-state & friends can publish non-breaking | ||
@@ -1196,7 +1267,2 @@ // compatibility releases | ||
} | ||
if (legacyComponent.remount) { | ||
component.remount((props) => { | ||
legacyComponent.remount(props, mkRenderArgs(component)); | ||
}); | ||
} | ||
if (legacyComponent.unmount) { | ||
@@ -1219,5 +1285,2 @@ component.unmount((props) => { | ||
}; | ||
function deriveComponentKey(key, componentIndex) { | ||
return `$Component${componentIndex}_${key}`; | ||
} | ||
/* | ||
@@ -1229,3 +1292,3 @@ Throw if component is a non-component | ||
if (warnOnLegacySyntax) { | ||
console.warn("Legacy component syntax is deprecated since v3.2.0 and will be removed in v5.0. The affected component was found here:"); | ||
console.warn("Legacy component syntax is deprecated in v3.2.0 and will be removed in v4.0. The affected component was found here:"); | ||
// Minification mangles component names so we have to settle for a | ||
@@ -1242,5 +1305,2 @@ // stacktrace. | ||
} | ||
function isNullOrUndefined(value) { | ||
return value === null || value === undefined; | ||
} | ||
function isString(val) { | ||
@@ -1271,25 +1331,17 @@ return typeof val === "string"; | ||
} | ||
function isKeyedElement(t) { | ||
return t.key !== undefined; | ||
} | ||
/** | ||
* 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); | ||
} | ||
function setNodeInfo(element, node, nodeIndex) { | ||
element.node = node; | ||
element.nodeIndex = nodeIndex; | ||
} | ||
function findNodeIndex(nodes, element, startSearchFrom) { | ||
if (element === undefined) | ||
/** | ||
* 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) { | ||
if (!element) | ||
return -1; | ||
for (let i = startSearchFrom; i < nodes.length; i++) { | ||
if (nodes[i] === element) { | ||
return i; | ||
} | ||
} | ||
return -1; | ||
return Array.from(nodes).indexOf(element); | ||
} | ||
@@ -1314,3 +1366,2 @@ /* JSX Types */ | ||
// This covers a consuming project using the forgo.createElement jsxFactory | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
export * as JSX from "./jsxTypes.js"; | ||
@@ -1325,7 +1376,5 @@ // If jsxTypes is imported using named imports, esbuild doesn't know how to | ||
// jsxFactory to createElement instead of forgo.createElement. | ||
// eslint-disable-next-line @typescript-eslint/no-namespace | ||
(function (createElement) { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
createElement.JSX = JSXTypes; | ||
})(createElement || (createElement = {})); | ||
//# sourceMappingURL=index.js.map |
@@ -1,2 +0,2 @@ | ||
import type { ForgoElementProps } from "./index.js"; | ||
import type { ForgoDOMElementProps } from "./index.js"; | ||
type Defaultize<Props, Defaults> = Props extends any ? Partial<Pick<Props, Extract<keyof Props, keyof Defaults>>> & Pick<Props, Exclude<keyof Props, keyof Defaults>> : never; | ||
@@ -303,3 +303,3 @@ export type LibraryManagedAttributes<Component, Props> = Component extends { | ||
export type WheelEventHandler<Target extends EventTarget> = EventHandler<TargetedWheelEvent<Target>>; | ||
export interface DOMAttributes<Target extends EventTarget> extends ForgoElementProps { | ||
export interface DOMAttributes<Target extends EventTarget> extends ForgoDOMElementProps { | ||
onload?: GenericEventHandler<Target>; | ||
@@ -306,0 +306,0 @@ onerror?: GenericEventHandler<Target>; |
{ | ||
"name": "forgo", | ||
"version": "4.0.6", | ||
"version": "4.1.0", | ||
"main": "./dist/forgo.min.js", | ||
@@ -18,6 +18,3 @@ "type": "module", | ||
"@types/source-map-support": "^0.5.6", | ||
"@typescript-eslint/eslint-plugin": "^5.49.0", | ||
"@typescript-eslint/parser": "^5.49.0", | ||
"esbuild": "^0.15.5", | ||
"eslint": "^8.32.0", | ||
"jsdom": "^20.0.0", | ||
@@ -28,3 +25,3 @@ "mocha": "^10.0.0", | ||
"source-map-support": "^0.5.21", | ||
"typescript": "^4.9.4" | ||
"typescript": "^4.8.2" | ||
}, | ||
@@ -34,3 +31,3 @@ "scripts": { | ||
"build": "npm run clean && npx tsc --emitDeclarationOnly && npx esbuild ./src/index.ts --minify --bundle --format=esm --sourcemap --target=es2015 --outfile=dist/forgo.min.js", | ||
"build-dev": "npm run clean && npx tsc", | ||
"build-dev": "npx tsc", | ||
"test": "npx tsc && npx mocha dist/test/test.js" | ||
@@ -37,0 +34,0 @@ }, |
104
README.md
@@ -70,3 +70,3 @@ # forgo | ||
return <p>Hello, world!</p>; | ||
}, | ||
} | ||
}); | ||
@@ -128,8 +128,7 @@ }; | ||
<button type="button" onclick={onclick}> | ||
The button has been clicked {clickCounter} times in {seconds}{" "} | ||
seconds | ||
The button has been clicked {clickCounter} times in {seconds} seconds | ||
</button> | ||
</div> | ||
); | ||
}, | ||
} | ||
}); | ||
@@ -162,3 +161,3 @@ | ||
return <p>Hello, {name}!</p>; | ||
}, | ||
} | ||
}); | ||
@@ -170,3 +169,3 @@ }; | ||
handlers), you'll need to annotate the generic types on both the constructor and | ||
the component. | ||
the component. | ||
@@ -186,3 +185,3 @@ _If you're handy with TypeScript, [we'd love a PR to infer the types!](https://github.com/forgojs/forgo/issues/68)_ | ||
return <p>Hello, {name}!</p>; | ||
}, | ||
} | ||
}); | ||
@@ -250,3 +249,3 @@ | ||
); | ||
}, | ||
} | ||
}); | ||
@@ -259,3 +258,3 @@ }; | ||
return <div>Hello {props.firstName}</div>; | ||
}, | ||
} | ||
}); | ||
@@ -273,3 +272,3 @@ }; | ||
return <NumberComponent myNumber={2} />; | ||
}, | ||
} | ||
}); | ||
@@ -337,8 +336,6 @@ }; | ||
<input type="text" ref={myInputRef} /> | ||
<button type="button" onclick={onClick}> | ||
Click me! | ||
</button> | ||
<button type="button" onclick={onClick}>Click me!</button> | ||
</div> | ||
); | ||
}, | ||
} | ||
}); | ||
@@ -375,3 +372,3 @@ }; | ||
); | ||
}, | ||
} | ||
}); | ||
@@ -415,3 +412,3 @@ }; | ||
); | ||
}, | ||
} | ||
}); | ||
@@ -459,3 +456,3 @@ }; | ||
); | ||
}, | ||
} | ||
}); | ||
@@ -486,3 +483,3 @@ | ||
return <div id="hello">Hello {props.firstName}</div>; | ||
}, | ||
} | ||
}); | ||
@@ -516,3 +513,3 @@ | ||
return <div>Hello {props.firstName}</div>; | ||
}, | ||
} | ||
}); | ||
@@ -551,3 +548,3 @@ | ||
return <div>Hello {props.firstName}</div>; | ||
}, | ||
} | ||
}); | ||
@@ -560,3 +557,3 @@ | ||
return component; | ||
}; | ||
} | ||
``` | ||
@@ -580,5 +577,5 @@ | ||
throw new Error("Some error occurred :("); | ||
}, | ||
} | ||
}); | ||
}; | ||
} | ||
@@ -601,5 +598,5 @@ // The first ancestor with an error() method defined will catch the error | ||
); | ||
}, | ||
} | ||
}); | ||
}; | ||
} | ||
``` | ||
@@ -621,3 +618,3 @@ | ||
return <div id="hello">Hello {props.firstName}</div>; | ||
}, | ||
} | ||
}); | ||
@@ -654,5 +651,5 @@ | ||
); | ||
}, | ||
} | ||
}); | ||
}; | ||
} | ||
``` | ||
@@ -704,5 +701,5 @@ | ||
); | ||
}, | ||
} | ||
}); | ||
}; | ||
} | ||
``` | ||
@@ -727,3 +724,3 @@ | ||
spam: [], | ||
unread: 0, | ||
unread: 0 | ||
}); | ||
@@ -738,5 +735,3 @@ | ||
<div> | ||
{mailboxState.messages.map((m) => ( | ||
<p>{m}</p> | ||
))} | ||
{mailboxState.messages.map((m) => <p>{m}</p>)} | ||
</div> | ||
@@ -751,3 +746,3 @@ ); | ||
); | ||
}, | ||
} | ||
}); | ||
@@ -763,3 +758,3 @@ | ||
return component; | ||
}; | ||
} | ||
@@ -796,5 +791,5 @@ async function updateInbox() { | ||
); | ||
}, | ||
} | ||
}); | ||
}; | ||
} | ||
``` | ||
@@ -823,5 +818,5 @@ | ||
return <p id="live-scores">Top score is {props.topscore}</p>; | ||
}, | ||
} | ||
}); | ||
}; | ||
} | ||
@@ -855,5 +850,5 @@ // Mount it on a DOM node usual | ||
return <div>Hello world</div>; | ||
}, | ||
} | ||
}); | ||
}; | ||
} | ||
@@ -864,2 +859,13 @@ // Get the html (string) and serve it via koa, express etc. | ||
## Manually adding elements to the DOM | ||
Forgo allows you to use the built-in browser DOM API to insert elements into the | ||
DOM tree rendered by a Forgo component. Forgo will ignore these elements. This | ||
is useful for working with charting libraries, such as D3. | ||
If you add unmanaged nodes as siblings to nodes which Forgo manages, Forgo | ||
pushes the unmanaged nodes towards the bottom of the sibling list when 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 | ||
@@ -889,3 +895,3 @@ | ||
); | ||
}, | ||
} | ||
}); | ||
@@ -1077,3 +1083,2 @@ | ||
## Deprecation of legacy component syntax is 3.2.0 | ||
In version 3.2.0, Forgo introduced a new syntax for components. This change | ||
@@ -1088,3 +1093,2 @@ makes Forgo easier to extend with reusable libraries, and makes it | ||
### Migrating | ||
Forgo components are now instances of the `Component` class, rather than | ||
@@ -1108,3 +1112,2 @@ freestanding object values. The `new Component` constructor accepts an object | ||
Before: | ||
```jsx | ||
@@ -1120,7 +1123,6 @@ const MyComponent = () => { | ||
}; | ||
}; | ||
} | ||
``` | ||
After: | ||
```jsx | ||
@@ -1130,3 +1132,3 @@ const MyComponent = () => { | ||
render() {}, | ||
error() {}, | ||
error() {} | ||
}); | ||
@@ -1140,3 +1142,3 @@ | ||
return component; | ||
}; | ||
} | ||
``` | ||
@@ -1149,2 +1151,2 @@ | ||
with ES modules. If you were using this previously, switch to the configurations | ||
discussed above. | ||
discussed above. |
@@ -1,2 +0,2 @@ | ||
import type { ForgoElementProps } from "./index.js"; | ||
import type { ForgoDOMElementProps } from "./index.js"; | ||
@@ -423,3 +423,3 @@ /* JSX Definitions */ | ||
export interface DOMAttributes<Target extends EventTarget> | ||
extends ForgoElementProps { | ||
extends ForgoDOMElementProps { | ||
// Image Events | ||
@@ -426,0 +426,0 @@ onload?: GenericEventHandler<Target>; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
409934
11
6042
1115
17