forgo-state
Advanced tools
Comparing version 1.2.0 to 1.3.0-beta.0
@@ -1,4 +0,5 @@ | ||
import { ForgoComponent, ForgoElementProps } from "forgo"; | ||
import { Component } from "forgo"; | ||
import type { ForgoComponent } from "forgo"; | ||
export declare function defineState<TState extends Record<string, any>>(state: TState): TState; | ||
export declare function bindToStates<TState, TProps extends ForgoElementProps>(states: TState[], component: ForgoComponent<TProps>): ForgoComponent<TProps>; | ||
export declare function bindToStateProps<TState, TProps extends ForgoElementProps>(stateBindings: [state: TState, propGetter?: (state: TState) => any[]][], component: ForgoComponent<TProps>): ForgoComponent<TProps>; | ||
export declare function bindToStates<TState>(states: TState[], component: Component<any>): void; | ||
export declare function bindToStateProps<TState>(stateBindings: [state: TState, propGetter?: (state: TState) => any[]][], suppliedComponent: Component<any> | ForgoComponent<any>): Component<any>; |
@@ -6,5 +6,5 @@ /* | ||
- They bind this state (the proxy object) to various components via bindToStates() and bindToStateProps() functions. | ||
- Since the proxy let's us capture changes to itself, we trigger component rerenders (on bound components) when that happens. | ||
- Since the proxy lets us capture changes to itself, we trigger component rerenders (on bound components) when that happens. | ||
*/ | ||
import { rerender, getForgoState, } from "forgo"; | ||
import { Component, getForgoState, legacyComponentSyntaxCompat } from "forgo"; | ||
const stateToComponentsMap = new Map(); | ||
@@ -14,15 +14,10 @@ export function defineState(state) { | ||
set(target, prop, value) { | ||
const entries = stateToComponentsMap.get(proxy); | ||
// if bound to the state directly, add for updation on any state change. | ||
const stateBoundComponentArgs = entries | ||
? entries | ||
.filter((x) => !x.propGetter) | ||
.map((x) => x.args) | ||
: []; | ||
const propBoundComponents = entries | ||
? entries.filter((x) => x.propGetter) | ||
: []; | ||
var _a; | ||
const entries = (_a = stateToComponentsMap.get(proxy)) !== null && _a !== void 0 ? _a : []; | ||
// If bound to the state directly, mark for update on any state change | ||
const stateBoundComponents = entries.filter((x) => !x.propGetter); | ||
const propBoundComponents = entries.filter((x) => x.propGetter); | ||
// Get the props before update | ||
let propBoundComponentArgs = propBoundComponents.map((x) => ({ | ||
args: x.args, | ||
const propBoundComponentProps = propBoundComponents.map((x) => ({ | ||
component: x.component, | ||
props: x.propGetter(target), | ||
@@ -32,4 +27,4 @@ })); | ||
// Get the props after update | ||
let updatedProps = propBoundComponents.map((x) => ({ | ||
args: x.args, | ||
const propBoundComponentPropsUpdated = propBoundComponents.map((x) => ({ | ||
component: x.component, | ||
props: x.propGetter(target), | ||
@@ -40,26 +35,31 @@ })); | ||
// So concat (a) and (b) | ||
const argsListToUpdate = stateBoundComponentArgs.concat(propBoundComponentArgs | ||
.filter((oldProp, i) => oldProp.props.some((p, j) => p !== updatedProps[i].props[j])) | ||
.map((x) => x.args)); | ||
// concat latest updates with pending updates. | ||
const componentsToUpdate = stateBoundComponents | ||
.concat(propBoundComponentProps | ||
.filter((oldProp, i) => oldProp.props.some((p, j) => p !== propBoundComponentPropsUpdated[i].props[j])) | ||
.map((x) => x)) | ||
.map((x) => x.component); | ||
// Concat latest updates with pending updates. | ||
const argsToUpdatePlusPendingArgs = Array.from(new Set([ | ||
...Array.from(argsToRenderInTheNextCycle), | ||
...argsListToUpdate, | ||
...Array.from(componentsToRenderInTheNextCycle), | ||
...componentsToUpdate, | ||
])); | ||
const componentStatesAndArgs = argsToUpdatePlusPendingArgs.map((x) => { | ||
const state = getForgoState(x.element.node); | ||
const componentStatesAndArgs = argsToUpdatePlusPendingArgs.map((component, index) => { | ||
const state = getForgoState(component.__internal.element.node); | ||
if (!state) { | ||
throw new Error("Missing state on node."); | ||
} | ||
else { | ||
return [state.components[x.element.componentIndex], x]; | ||
const componentState = state.components[component.__internal.element.componentIndex]; | ||
if (!componentState) { | ||
throw new Error("Attempted to update a component that doesn't exist anymore"); | ||
} | ||
return [componentState, component]; | ||
}); | ||
// If a parent component is already rerendering, | ||
// don't queue the child rerender. | ||
const componentsToUpdate = componentStatesAndArgs.filter((item) => { | ||
const [componentState, args] = item; | ||
let node = args.element.node; | ||
const dedupedComponentsToUpdate = componentStatesAndArgs.filter((item) => { | ||
const [componentState, component] = item; | ||
let node = component.__internal.element | ||
.node; | ||
let state = getForgoState(node); | ||
let parentStates = state.components.slice(0, args.element.componentIndex); | ||
let parentStates = state.components.slice(0, component.__internal.element.componentIndex); | ||
while (node && state) { | ||
@@ -79,5 +79,3 @@ if (parentStates.some((x) => componentStatesAndArgs.some(([compStateInArray]) => compStateInArray.component === x.component))) { | ||
}); | ||
for (const [, args] of componentsToUpdate) { | ||
argsToRenderInTheNextCycle.add(args); | ||
} | ||
dedupedComponentsToUpdate.forEach(([, component]) => componentsToRenderInTheNextCycle.add(component)); | ||
setTimeout(() => { | ||
@@ -95,62 +93,57 @@ doRender(); | ||
// had queued, plus everything the subrender enqueues | ||
const argsToRenderInTheNextCycle = new Set(); | ||
const componentsToRenderInTheNextCycle = new Set(); | ||
function doRender() { | ||
if (argsToRenderInTheNextCycle.size > 0) { | ||
for (const args of argsToRenderInTheNextCycle) { | ||
if (args.element.node && args.element.node.isConnected) { | ||
// Dequeue the component before the render, so that if the component | ||
// triggers more renders of itself they don't get no-op'd | ||
argsToRenderInTheNextCycle.delete(args); | ||
rerender(args.element); | ||
} | ||
} | ||
} | ||
Array.from(componentsToRenderInTheNextCycle).forEach((component) => { | ||
// Dequeue the component before the render, so that if the component | ||
// triggers more renders of itself they don't get no-op'd | ||
componentsToRenderInTheNextCycle.delete(component); | ||
component.update(); | ||
}); | ||
} | ||
export function bindToStates(states, component) { | ||
return bindToStateProps(states.map((state) => [state, undefined]), component); | ||
bindToStateProps(states.map((state) => [state, undefined]), component); | ||
} | ||
export function bindToStateProps(stateBindings, component) { | ||
const wrappedComponent = Object.assign(Object.assign({}, component), { mount(props, args) { | ||
for (const [state, propGetter] of stateBindings) { | ||
let entries = stateToComponentsMap.get(state); | ||
if (!entries) { | ||
entries = []; | ||
stateToComponentsMap.set(state, entries); | ||
} | ||
if (propGetter) { | ||
const newEntry = { | ||
component: wrappedComponent, | ||
propGetter, | ||
args, | ||
}; | ||
entries.push(newEntry); | ||
} | ||
else { | ||
const newEntry = { | ||
component: wrappedComponent, | ||
args, | ||
}; | ||
entries.push(newEntry); | ||
} | ||
export function bindToStateProps(stateBindings, suppliedComponent) { | ||
const component = suppliedComponent instanceof Component | ||
? suppliedComponent | ||
: legacyComponentSyntaxCompat(suppliedComponent); | ||
component.mount(() => { | ||
for (const [state, propGetter] of stateBindings) { | ||
let entries = stateToComponentsMap.get(state); | ||
if (!entries) { | ||
entries = []; | ||
stateToComponentsMap.set(state, entries); | ||
} | ||
if (component.mount) { | ||
component.mount(props, args); | ||
if (propGetter) { | ||
const newEntry = { | ||
component, | ||
propGetter, | ||
}; | ||
entries.push(newEntry); | ||
} | ||
}, | ||
unmount(props, args) { | ||
for (const [state] of stateBindings) { | ||
let entry = stateToComponentsMap.get(state); | ||
if (entry) { | ||
stateToComponentsMap.set(state, entry.filter((x) => x.component !== wrappedComponent)); | ||
} | ||
else { | ||
throw new Error("Component entry missing in state map."); | ||
} | ||
else { | ||
const newEntry = { | ||
component, | ||
}; | ||
entries.push(newEntry); | ||
} | ||
if (component.unmount) { | ||
component.unmount(props, args); | ||
} | ||
}); | ||
component.unmount(() => { | ||
for (const [state] of stateBindings) { | ||
let entry = stateToComponentsMap.get(state); | ||
if (entry) { | ||
entry.splice(entry.findIndex((x) => x.component === component), 1); | ||
// This could be optimized into an unshift / pop at the specific index | ||
componentsToRenderInTheNextCycle.delete(component); | ||
} | ||
} }); | ||
return wrappedComponent; | ||
else { | ||
throw new Error("Component entry missing in state map."); | ||
} | ||
} | ||
}); | ||
// TODO: We only do this to avoid breaking compat with the legacy component | ||
// syntax, but with Forgo v4 it won't be necessary. | ||
return component; | ||
} | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "forgo-state", | ||
"version": "1.2.0", | ||
"version": "1.3.0-beta.0", | ||
"type": "module", | ||
@@ -13,14 +13,16 @@ "main": "./dist/index.js", | ||
"peerDependencies": { | ||
"forgo": "^3.1.0" | ||
"forgo": "^3.2.0-alpha.0" | ||
}, | ||
"devDependencies": { | ||
"@types/jsdom": "^16.2.13", | ||
"@types/mocha": "^9.0.0", | ||
"@types/jsdom": "^16.2.14", | ||
"@types/mocha": "^9.1.0", | ||
"@types/should": "^13.0.0", | ||
"@types/source-map-support": "^0.5.4", | ||
"esm": "^3.2.25", | ||
"jsdom": "^17.0.0", | ||
"mocha": "^9.0.3", | ||
"jsdom": "^19.0.0", | ||
"mocha": "^9.2.2", | ||
"rimraf": "^3.0.2", | ||
"should": "^13.2.3", | ||
"typescript": "^4.3.5" | ||
"source-map-support": "^0.5.21", | ||
"typescript": "^4.6.3" | ||
}, | ||
@@ -27,0 +29,0 @@ "scripts": { |
289
src/index.ts
@@ -6,26 +6,22 @@ /* | ||
- They bind this state (the proxy object) to various components via bindToStates() and bindToStateProps() functions. | ||
- Since the proxy let's us capture changes to itself, we trigger component rerenders (on bound components) when that happens. | ||
- Since the proxy lets us capture changes to itself, we trigger component rerenders (on bound components) when that happens. | ||
*/ | ||
import { | ||
ForgoRenderArgs, | ||
import { Component, getForgoState, legacyComponentSyntaxCompat } from "forgo"; | ||
import type { | ||
ForgoComponent, | ||
ForgoElementProps, | ||
rerender, | ||
getForgoState, | ||
NodeAttachedState, | ||
NodeAttachedComponentState, | ||
NodeAttachedState, | ||
} from "forgo"; | ||
type StateBoundComponentInfo<TProps extends ForgoElementProps> = { | ||
component: ForgoComponent<TProps>; | ||
args: ForgoRenderArgs; | ||
}; | ||
interface StateBoundComponentInfo { | ||
component: Component; | ||
} | ||
type PropertyBoundComponentInfo<TState, TProps extends ForgoElementProps> = { | ||
interface PropertyBoundComponentInfo<TState> extends StateBoundComponentInfo { | ||
propGetter: (state: TState) => any[]; | ||
} & StateBoundComponentInfo<TProps>; | ||
} | ||
const stateToComponentsMap: Map<any, StateBoundComponentInfo<any>[]> = | ||
new Map(); | ||
const stateToComponentsMap: Map<any, StateBoundComponentInfo[]> = new Map(); | ||
@@ -37,25 +33,17 @@ export function defineState<TState extends Record<string, any>>( | ||
set(target: TState, prop: string & keyof TState, value: any) { | ||
const entries = stateToComponentsMap.get(proxy); | ||
const entries = stateToComponentsMap.get(proxy) ?? []; | ||
// if bound to the state directly, add for updation on any state change. | ||
const stateBoundComponentArgs: ForgoRenderArgs[] = entries | ||
? entries | ||
.filter( | ||
(x) => !(x as PropertyBoundComponentInfo<TState, any>).propGetter | ||
) | ||
.map((x) => x.args) | ||
: []; | ||
// If bound to the state directly, mark for update on any state change | ||
const stateBoundComponents: StateBoundComponentInfo[] = entries.filter( | ||
(x) => !(x as PropertyBoundComponentInfo<TState>).propGetter | ||
); | ||
const propBoundComponents = entries | ||
? entries.filter( | ||
(x) => (x as PropertyBoundComponentInfo<TState, any>).propGetter | ||
) | ||
: []; | ||
const propBoundComponents = entries.filter( | ||
(x) => (x as PropertyBoundComponentInfo<TState>).propGetter | ||
); | ||
// Get the props before update | ||
let propBoundComponentArgs = propBoundComponents.map((x) => ({ | ||
args: x.args, | ||
props: (x as PropertyBoundComponentInfo<TState, any>).propGetter( | ||
target | ||
), | ||
const propBoundComponentProps = propBoundComponents.map((x) => ({ | ||
component: x.component, | ||
props: (x as PropertyBoundComponentInfo<TState>).propGetter(target), | ||
})); | ||
@@ -66,7 +54,5 @@ | ||
// Get the props after update | ||
let updatedProps = propBoundComponents.map((x) => ({ | ||
args: x.args, | ||
props: (x as PropertyBoundComponentInfo<TState, any>).propGetter( | ||
target | ||
), | ||
const propBoundComponentPropsUpdated = propBoundComponents.map((x) => ({ | ||
component: x.component, | ||
props: (x as PropertyBoundComponentInfo<TState>).propGetter(target), | ||
})); | ||
@@ -77,15 +63,19 @@ | ||
// So concat (a) and (b) | ||
const argsListToUpdate = stateBoundComponentArgs.concat( | ||
propBoundComponentArgs | ||
.filter((oldProp, i) => | ||
oldProp.props.some((p, j) => p !== updatedProps[i].props[j]) | ||
) | ||
.map((x) => x.args) | ||
); | ||
const componentsToUpdate = stateBoundComponents | ||
.concat( | ||
propBoundComponentProps | ||
.filter((oldProp, i) => | ||
oldProp.props.some( | ||
(p, j) => p !== propBoundComponentPropsUpdated[i].props[j] | ||
) | ||
) | ||
.map((x) => x) | ||
) | ||
.map((x) => x.component); | ||
// concat latest updates with pending updates. | ||
// Concat latest updates with pending updates. | ||
const argsToUpdatePlusPendingArgs = Array.from( | ||
new Set([ | ||
...Array.from(argsToRenderInTheNextCycle), | ||
...argsListToUpdate, | ||
new Set<Component>([ | ||
...Array.from(componentsToRenderInTheNextCycle), | ||
...componentsToUpdate, | ||
]) | ||
@@ -96,10 +86,19 @@ ); | ||
NodeAttachedComponentState<any>, | ||
ForgoRenderArgs | ||
][] = argsToUpdatePlusPendingArgs.map((x) => { | ||
const state = getForgoState(x.element.node as ChildNode); | ||
Component | ||
][] = argsToUpdatePlusPendingArgs.map((component, index) => { | ||
const state = getForgoState( | ||
component.__internal.element.node as ChildNode | ||
); | ||
if (!state) { | ||
throw new Error("Missing state on node."); | ||
} else { | ||
return [state.components[x.element.componentIndex], x]; | ||
} | ||
const componentState = | ||
state.components[component.__internal.element.componentIndex]; | ||
if (!componentState) { | ||
throw new Error( | ||
"Attempted to update a component that doesn't exist anymore" | ||
); | ||
} | ||
return [componentState, component]; | ||
}); | ||
@@ -109,38 +108,41 @@ | ||
// don't queue the child rerender. | ||
const componentsToUpdate = componentStatesAndArgs.filter((item) => { | ||
const [componentState, args] = item; | ||
const dedupedComponentsToUpdate = componentStatesAndArgs.filter( | ||
(item) => { | ||
const [componentState, component] = item; | ||
let node: ChildNode | null = args.element.node as ChildNode; | ||
let state: NodeAttachedState | undefined = getForgoState(node); | ||
let parentStates = (state as NodeAttachedState).components.slice( | ||
0, | ||
args.element.componentIndex | ||
); | ||
while (node && state) { | ||
if ( | ||
parentStates.some((x) => | ||
componentStatesAndArgs.some( | ||
([compStateInArray]) => | ||
compStateInArray.component === x.component | ||
let node: ChildNode | null = component.__internal.element | ||
.node as ChildNode; | ||
let state: NodeAttachedState | undefined = getForgoState(node); | ||
let parentStates = (state as NodeAttachedState).components.slice( | ||
0, | ||
component.__internal.element.componentIndex | ||
); | ||
while (node && state) { | ||
if ( | ||
parentStates.some((x) => | ||
componentStatesAndArgs.some( | ||
([compStateInArray]) => | ||
compStateInArray.component === x.component | ||
) | ||
) | ||
) | ||
) { | ||
return false; | ||
} | ||
node = node.parentElement; | ||
if (node) { | ||
state = getForgoState(node); | ||
if (state) { | ||
parentStates = state.components.filter( | ||
(x) => x !== componentState | ||
); | ||
) { | ||
return false; | ||
} | ||
node = node.parentElement; | ||
if (node) { | ||
state = getForgoState(node); | ||
if (state) { | ||
parentStates = state.components.filter( | ||
(x) => x !== componentState | ||
); | ||
} | ||
} | ||
} | ||
return true; | ||
} | ||
return true; | ||
}); | ||
); | ||
for (const [, args] of componentsToUpdate) { | ||
argsToRenderInTheNextCycle.add(args); | ||
} | ||
dedupedComponentsToUpdate.forEach(([, component]) => | ||
componentsToRenderInTheNextCycle.add(component) | ||
); | ||
@@ -163,23 +165,19 @@ setTimeout(() => { | ||
// had queued, plus everything the subrender enqueues | ||
const argsToRenderInTheNextCycle = new Set<ForgoRenderArgs>(); | ||
const componentsToRenderInTheNextCycle = new Set<Component>(); | ||
function doRender() { | ||
if (argsToRenderInTheNextCycle.size > 0) { | ||
for (const args of argsToRenderInTheNextCycle) { | ||
if (args.element.node && args.element.node.isConnected) { | ||
// Dequeue the component before the render, so that if the component | ||
// triggers more renders of itself they don't get no-op'd | ||
argsToRenderInTheNextCycle.delete(args); | ||
Array.from(componentsToRenderInTheNextCycle).forEach((component) => { | ||
// Dequeue the component before the render, so that if the component | ||
// triggers more renders of itself they don't get no-op'd | ||
componentsToRenderInTheNextCycle.delete(component); | ||
rerender(args.element); | ||
} | ||
} | ||
} | ||
component.update(); | ||
}); | ||
} | ||
export function bindToStates<TState, TProps extends ForgoElementProps>( | ||
export function bindToStates<TState>( | ||
states: TState[], | ||
component: ForgoComponent<TProps> | ||
): ForgoComponent<TProps> { | ||
return bindToStateProps( | ||
component: Component<any> | ||
): void { | ||
bindToStateProps( | ||
states.map((state) => [state, undefined]), | ||
@@ -190,60 +188,57 @@ component | ||
export function bindToStateProps<TState, TProps extends ForgoElementProps>( | ||
export function bindToStateProps<TState>( | ||
stateBindings: [state: TState, propGetter?: (state: TState) => any[]][], | ||
component: ForgoComponent<TProps> | ||
): ForgoComponent<TProps> { | ||
const wrappedComponent = { | ||
...component, | ||
mount(props: TProps, args: ForgoRenderArgs) { | ||
for (const [state, propGetter] of stateBindings) { | ||
let entries = stateToComponentsMap.get(state); | ||
suppliedComponent: Component<any> | ForgoComponent<any> | ||
): Component<any> { | ||
const component = | ||
suppliedComponent instanceof Component | ||
? suppliedComponent | ||
: legacyComponentSyntaxCompat(suppliedComponent); | ||
if (!entries) { | ||
entries = []; | ||
stateToComponentsMap.set(state, entries); | ||
} | ||
component.mount(() => { | ||
for (const [state, propGetter] of stateBindings) { | ||
let entries = stateToComponentsMap.get(state); | ||
if (propGetter) { | ||
const newEntry: PropertyBoundComponentInfo<TState, TProps> = { | ||
component: wrappedComponent, | ||
propGetter, | ||
args, | ||
}; | ||
if (!entries) { | ||
entries = []; | ||
stateToComponentsMap.set(state, entries); | ||
} | ||
entries.push(newEntry); | ||
} else { | ||
const newEntry: StateBoundComponentInfo<TProps> = { | ||
component: wrappedComponent, | ||
args, | ||
}; | ||
if (propGetter) { | ||
const newEntry: PropertyBoundComponentInfo<TState> = { | ||
component, | ||
propGetter, | ||
}; | ||
entries.push(newEntry); | ||
} | ||
} | ||
entries.push(newEntry); | ||
} else { | ||
const newEntry: StateBoundComponentInfo = { | ||
component, | ||
}; | ||
if (component.mount) { | ||
component.mount(props, args); | ||
entries.push(newEntry); | ||
} | ||
}, | ||
unmount(props: TProps, args: ForgoRenderArgs) { | ||
for (const [state] of stateBindings) { | ||
let entry = stateToComponentsMap.get(state); | ||
} | ||
}); | ||
if (entry) { | ||
stateToComponentsMap.set( | ||
state, | ||
entry.filter((x) => x.component !== wrappedComponent) | ||
); | ||
} else { | ||
throw new Error("Component entry missing in state map."); | ||
} | ||
} | ||
component.unmount(() => { | ||
for (const [state] of stateBindings) { | ||
let entry = stateToComponentsMap.get(state); | ||
if (component.unmount) { | ||
component.unmount(props, args); | ||
if (entry) { | ||
entry.splice( | ||
entry.findIndex((x) => x.component === component), | ||
1 | ||
); | ||
// This could be optimized into an unshift / pop at the specific index | ||
componentsToRenderInTheNextCycle.delete(component); | ||
} else { | ||
throw new Error("Component entry missing in state map."); | ||
} | ||
}, | ||
}; | ||
} | ||
}); | ||
return wrappedComponent; | ||
// TODO: We only do this to avoid breaking compat with the legacy component | ||
// syntax, but with Forgo v4 it won't be necessary. | ||
return component; | ||
} |
Sorry, the diff of this file is not supported yet
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
24268
11
364
2