UNPKG

mobx-bonsai

Version:

A fast lightweight alternative to MobX-State-Tree + Y.js two-way binding

281 lines (231 loc) 6.64 kB
import { action, computed, createAtom, IAtom, IComputedValue, observable } from "mobx" import { getOrCreate } from "../utils/mapUtils" import { assertIsNode, isNode } from "./node" import { getParent } from "./tree/getParent" /** * A context. */ export interface Context<T> { /** * Gets the context default value. * * @returns */ getDefault(): T /** * Sets the context default value. * @param value */ setDefault(value: T): void /** * Sets the context default value resolver. * @param valueFn */ setDefaultComputed(valueFn: () => T): void /** * Gets the context value for a given node. * @param node */ get(node: object): T /** * Gets node that will provide the context value, or `undefined` * if it comes from the default. * @param node */ getProviderNode(node: object): object | undefined /** * Sets the context value for a given node, effectively making it a provider. * @param node * @param value */ set(node: object, value: T): void /** * Sets the context value resolver for a given node, effectively making it a provider. * @param node * @param valueFn */ setComputed(node: object, valueFn: () => T): void /** * Unsets the context value for a given node, therefore it won't be a provider anymore. * @param node */ unset(node: object): void /** * Applies a value override while the given function is running and, if a node is returned, * sets the node as a provider of the value. * * @template R * @param fn Function to run. * @param value Value to apply. * @returns The value returned from the function. */ apply<R>(fn: () => R, value: T): R /** * Applies a computed value override while the given function is running and, if a node is returned, * sets the node as a provider of the computed value. * * @template R * @param fn Function to run. * @param value Value to apply. * @returns The value returned from the function. */ applyComputed<R>(fn: () => R, valueFn: () => T): R } type ContextValue<T> = | { type: "value" value: T } | { type: "computed" value: IComputedValue<T> } function resolveContextValue<T>(contextValue: ContextValue<T>): T { if (contextValue.type === "value") { return contextValue.value } else { return contextValue.value.get() } } const createContextValueAtom = () => createAtom("contextValue") class ContextClass<T> implements Context<T> { private defaultContextValue = observable.box<ContextValue<T>>(undefined, { deep: false }) private overrideContextValue = observable.box<ContextValue<T> | undefined>(undefined, { deep: false, }) private readonly nodeContextValue = new WeakMap<object, ContextValue<T>>() private readonly nodeAtom = new WeakMap<object, IAtom>() private reportNodeAtomObserved(node: object) { getOrCreate(this.nodeAtom, node, createContextValueAtom).reportObserved() } private reportNodeAtomChanged(node: object) { this.nodeAtom.get(node)?.reportChanged() } private internalGet(node: object): T { this.reportNodeAtomObserved(node) const obsForNode = this.nodeContextValue.get(node) if (obsForNode) { return resolveContextValue(obsForNode) } const parent = getParent(node) if (!parent) { const overrideValue = this.overrideContextValue.get() if (overrideValue) { return resolveContextValue(overrideValue) } return this.getDefault() } return this.internalGet(parent) } get(node: object) { assertIsNode(node, "node") return this.internalGet(node) } private internalGetProviderNode(node: object): object | undefined { this.reportNodeAtomObserved(node) const obsForNode = this.nodeContextValue.get(node) if (obsForNode) { return node } const parent = getParent(node) if (!parent) { return undefined } return this.internalGetProviderNode(parent) } getProviderNode(node: object): object | undefined { assertIsNode(node, "node") return this.internalGetProviderNode(node) } getDefault(): T { return resolveContextValue(this.defaultContextValue.get()!) } setDefault = action((value: T) => { this.defaultContextValue.set({ type: "value", value, }) }) setDefaultComputed = action((valueFn: () => T) => { this.defaultContextValue.set({ type: "computed", value: computed(valueFn), }) }) set = action((node: object, value: T) => { assertIsNode(node, "node") this.nodeContextValue.set(node, { type: "value", value, }) this.reportNodeAtomChanged(node) }) private _setComputed(node: object, computedValueFn: IComputedValue<T>) { assertIsNode(node, "node") this.nodeContextValue.set(node, { type: "computed", value: computedValueFn }) this.reportNodeAtomChanged(node) } setComputed = action((node: object, valueFn: () => T) => { this._setComputed(node, computed(valueFn)) }) unset = action((node: object) => { assertIsNode(node, "node") this.nodeContextValue.delete(node) this.reportNodeAtomChanged(node) }) apply = action(<R>(fn: () => R, value: T): R => { const old = this.overrideContextValue.get() this.overrideContextValue.set({ type: "value", value, }) try { const ret = fn() if (isNode(ret)) { this.set(ret as object, value) } return ret } finally { this.overrideContextValue.set(old) } }) applyComputed = action(<R>(fn: () => R, valueFn: () => T): R => { const computedValueFn = computed(valueFn) const old = this.overrideContextValue.get() this.overrideContextValue.set({ type: "computed", value: computedValueFn, }) try { const ret = fn() if (isNode(ret)) { this._setComputed(ret as object, computedValueFn) } return ret } finally { this.overrideContextValue.set(old) } }) constructor(defaultValue?: T) { this.setDefault(defaultValue as T) } } /** * Creates a new context with no default value, thus making its default value undefined. * * @template T Context value type. * @returns */ export function createContext<T>(): Context<T | undefined> /** * Creates a new context with a default value. * * @template T Context value type. * @param defaultValue * @returns */ export function createContext<T>(defaultValue: T): Context<T> // base export function createContext<T>(defaultValue?: T): Context<T> { return new ContextClass(defaultValue) }