Open petermakeswebsites opened 5 months ago
I haven't actually tested this, so I'm not 100% it will work. But this is my alpha.
I will say, I noticed it doesn't really fit the pattern of the other history though, and I'm curious why the team chose to make some design decisions.
I like to stay as close to the primitives (runes) as possible. Using abstractions like box()
and watch()
, although useful, I think add an unnecessary extra layer of complexity on top. But maybe I don't fully understand their value. I wonder why you are using these in higher level functions?
I also use classes instead of functions for two reasons. First, it's more readable. Second, the methods are in the prototype so they are not re-instantiated every time a new one is created. Reduce reuse recycle ♻️!
You can create a BranchedHistory as follows:
// Create state
const history = useBranchedStateHistory("original state")
// Change state
history.state = "some new state"
// Undo
history.undo() // history.state == "original state"
// Create new state
history.state = "a new branch!"
// Undo again
history.undo() // history.state == "original state"
// Get children
children = [...history.children] // [BranchNode {value: "some new state"}, BranchNode {value: "a new branch!"} ]
// Redo
history.redo() // Remembers the last "undid" branch, ergo history.state == "a new branch!"
// Go to specific node
history.goto(children[0]) // history.state == "original state"
import { Set } from "svelte/reactivity";
/**
* Represents a specific state in the timeline
*/
class BranchNode<T> {
/**
* This is set to the node that we called undo from, so we know where to
* re-do. This should be reactive because it may change.
*/
public redoTarget = $state<BranchNode<T>>();
/**
* Children may change, so we're using Svelte's built-in reactivity set
*/
public readonly children = new Set<BranchNode<T>>();
constructor(
public readonly value: T,
/**
* parent will never change, and therefor does not need to be stateful
*/
public readonly parent: BranchNode<T> | undefined = undefined
) {}
}
class BranchedHistory<T> {
public readonly root: BranchNode<T>;
public node = $state<BranchNode<T>>() as BranchNode<T>;
constructor(def: T) {
this.root = new BranchNode(def);
this.node = this.root;
}
public get state() {
return this.node!.value;
}
public set state(v: T) {
const parentNode = this.node!;
this.node = new BranchNode(v, parentNode);
parentNode.children.add(this.node);
}
canUndo = $derived(!!this.node.parent);
canRedo = $derived(!!this.node.redoTarget);
/**
* Branches coming off this node
*/
children = $derived(this.node.children);
/**
* Undefined if root note
*/
parent = $derived(this.node.parent);
/**
* Go to the selected node
* @param node
*/
public goto(node: BranchNode<T>) {
this.node = node;
}
public undo() {
if (!this.node.parent) return;
const redoNode = this.node;
this.node = this.node.parent;
this.node.redoTarget = redoNode;
}
public redo() {
if (!this.node.redoTarget) return;
this.node = this.node.redoTarget;
}
}
export function useBranchedStateHistory<T>(def: T) {
return new BranchedHistory(def);
}
The box
abstraction is useful when you want to return boxed values directly, which allows destructuring.
const { x, y } = useMouse();
For returning values directly, I agree that $state
and $derived
are good enough.
Hey @petermakeswebsites , this could really be interesting! Do you want to submit a PR?
Describe the feature in detail (code, mocks, or screenshots encouraged)
For a Svelte 5 app I made, I created a branched state history, not dissimilar to Git branches. I'd be happy to create an abstraction of this and add it to this lib. The idea is essentially as follows:
Is this something you would all find useful?
What type of pull request would this be?
New Feature
Provide relevant links or additional information.
No response