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
text/typescript
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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
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
}