UNPKG

mobx-keystone

Version:

A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more

177 lines (153 loc) 5.29 kB
import { action, computed } from "mobx" import { pathToTargetPathIds } from "../actionMiddlewares/utils" import { resolvePath, resolvePathCheckingIds, skipIdChecking } from "../parent/path" import type { Path } from "../parent/pathTypes" import { applyPatches } from "../patch/applyPatches" import { applySnapshot } from "../snapshot/applySnapshot" import { fromSnapshot } from "../snapshot/fromSnapshot" import { getSnapshot } from "../snapshot/getSnapshot" import { assertTweakedObject } from "../tweaker/core" import { failure } from "../utils" import { deepEquals } from "./deepEquals" /** * A class with the implementationm of draft. * Use `draft` to create an instance of this class. * * @template T Data type. */ export class Draft<T extends object> { /** * Draft data object. */ readonly data: T /** * Commits current draft changes to the original object. */ @action commit(): void { applySnapshot(this.originalData, getSnapshot(this.data)) } /** * Partially commits current draft changes to the original object. * If the path cannot be resolved in either the draft or the original object it will throw. * Note that model IDs are checked to be the same when resolving the paths. * * @param path Path to commit. */ @action commitByPath(path: Path): void { const draftTarget = resolvePath(this.data, path) if (!draftTarget.resolved) { throw failure(`path ${JSON.stringify(path)} could not be resolved in draft object`) } const draftPathIds = pathToTargetPathIdsIgnoringLast(this.data, path) const originalTarget = resolvePathCheckingIds(this.originalData, path, draftPathIds) if (!originalTarget.resolved) { throw failure(`path ${JSON.stringify(path)} could not be resolved in original object`) } applyPatches(this.originalData, [ { path, op: "replace", value: getSnapshot(draftTarget.value), }, ]) } /** * Resets the draft to be an exact copy of the current state of the original object. */ @action reset(): void { applySnapshot(this.data, this.originalSnapshot) } /** * Partially resets current draft changes to be the same as the original object. * If the path cannot be resolved in either the draft or the original object it will throw. * Note that model IDs are checked to be the same when resolving the paths. * * @param path Path to reset. */ @action resetByPath(path: Path): void { const originalTarget = resolvePath(this.originalData, path) if (!originalTarget.resolved) { throw failure(`path ${JSON.stringify(path)} could not be resolved in original object`) } const originalPathIds = pathToTargetPathIdsIgnoringLast(this.originalData, path) const draftTarget = resolvePathCheckingIds(this.data, path, originalPathIds) if (!draftTarget.resolved) { throw failure(`path ${JSON.stringify(path)} could not be resolved in draft object`) } applyPatches(this.data, [ { path, op: "replace", value: getSnapshot(originalTarget.value), }, ]) } /** * Returns `true` if the draft has changed compared to the original object, `false` otherwise. */ @computed get isDirty(): boolean { return !deepEquals(getSnapshot(this.data), this.originalSnapshot) } /** * Returns `true` if the value at the given path of the draft has changed compared to the original object. * If the path cannot be resolved in the draft it will throw. * If the path cannot be resolved in the original object it will return `true`. * Note that model IDs are checked to be the same when resolving the paths. * * @param path Path to check. */ isDirtyByPath(path: Path): boolean { const draftTarget = resolvePath(this.data, path) if (!draftTarget.resolved) { throw failure(`path ${JSON.stringify(path)} could not be resolved in draft object`) } const draftPathIds = pathToTargetPathIdsIgnoringLast(this.data, path) const originalTarget = resolvePathCheckingIds(this.originalData, path, draftPathIds) if (!originalTarget.resolved) { return true } return !deepEquals(draftTarget.value, originalTarget.value) } /** * Original data object. */ readonly originalData: T @computed private get originalSnapshot() { return getSnapshot(this.originalData) } /** * Creates an instance of Draft. * Do not use directly, use `draft` instead. * * @param original */ constructor(original: T) { assertTweakedObject(original, "original") this.originalData = original this.data = fromSnapshot(this.originalSnapshot, { generateNewIds: false }) } } /** * Creates a draft copy of a tree node and all its children. * * @template T Data type. * @param original Original node. * @returns The draft object. */ export function draft<T extends object>(original: T): Draft<T> { return new Draft(original) } function pathToTargetPathIdsIgnoringLast(root: any, path: Path) { const pathIds: (string | null | typeof skipIdChecking)[] = pathToTargetPathIds(root, path) if (pathIds.length > 0) { // never check the last object id pathIds[pathIds.length - 1] = skipIdChecking } return pathIds }