mobx-keystone
Version:
A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more
140 lines (117 loc) • 4.1 kB
text/typescript
import { action, reaction, runInAction } from "mobx"
import { assertTweakedObject } from "../tweaker/core"
import { assertIsFunction } from "../utils"
import { getChildrenObjects } from "./getChildrenObjects"
/**
* Runs a callback everytime a new object is attached to a given node.
* The callback can optionally return a disposer which will be run when the child is detached.
*
* The optional options parameter accepts an object with the following options:
* - `deep: boolean` (default: `false`) - true if the callback should be run for all children deeply
* or false if it it should only run for shallow children.
* - `fireForCurrentChildren: boolean` (default: `true`) - true if the callback should be immediately
* called for currently attached children, false if only for future attachments.
*
* Returns a disposer, which has a boolean parameter which should be true if pending detachment
* callbacks should be run or false otherwise.
*
* @param target Function that returns the object whose children should be tracked.
* @param fn Callback called when a child is attached to the target object.
* @param [options]
* @returns
*/
export function onChildAttachedTo(
target: () => object,
fn: (child: object) => (() => void) | void,
options?: {
deep?: boolean
fireForCurrentChildren?: boolean
}
): (runDetachDisposers: boolean) => void {
assertIsFunction(target, "target")
assertIsFunction(fn, "fn")
const opts = {
deep: false,
fireForCurrentChildren: true,
...options,
}
const detachDisposers = new WeakMap<object, () => void>()
const runDetachDisposer = (n: object) => {
const detachDisposer = detachDisposers.get(n)
if (detachDisposer) {
detachDisposers.delete(n)
detachDisposer()
}
}
const addDetachDisposer = (n: object, disposer: (() => void) | void) => {
if (disposer) {
detachDisposers.set(n, action(disposer))
}
}
const getChildrenObjectOpts = { deep: opts.deep }
const getCurrentChildren = () => {
const t = target()
assertTweakedObject(t, "target()")
const children = getChildrenObjects(t, getChildrenObjectOpts)
const set = new Set<object>()
const iter = children.values()
let cur = iter.next()
while (!cur.done) {
set.add(cur.value)
cur = iter.next()
}
return set
}
const currentChildren = opts.fireForCurrentChildren ? new Set<object>() : getCurrentChildren()
const disposer = reaction(
() => getCurrentChildren(),
(newChildren) => {
const disposersToRun: object[] = []
// find dead
const currentChildrenIter = currentChildren.values()
let currentChildrenCur = currentChildrenIter.next()
while (!currentChildrenCur.done) {
const n = currentChildrenCur.value
if (!newChildren.has(n)) {
currentChildren.delete(n)
// we should run it in inverse order
disposersToRun.push(n)
}
currentChildrenCur = currentChildrenIter.next()
}
if (disposersToRun.length > 0) {
for (let i = disposersToRun.length - 1; i >= 0; i--) {
runDetachDisposer(disposersToRun[i])
}
}
// find new
const newChildrenIter = newChildren.values()
let newChildrenCur = newChildrenIter.next()
while (!newChildrenCur.done) {
const n = newChildrenCur.value
if (!currentChildren.has(n)) {
currentChildren.add(n)
const detachAction = runInAction(() => fn(n))
addDetachDisposer(n, detachAction)
}
newChildrenCur = newChildrenIter.next()
}
},
{
fireImmediately: true,
}
)
return (runDetachDisposers: boolean) => {
disposer()
if (runDetachDisposers) {
const currentChildrenIter = currentChildren.values()
let currentChildrenCur = currentChildrenIter.next()
while (!currentChildrenCur.done) {
const n = currentChildrenCur.value
runDetachDisposer(n)
currentChildrenCur = currentChildrenIter.next()
}
}
currentChildren.clear()
}
}