@thi.ng/atom
About
Clojure inspired mutable wrappers for (usually) immutable values, with support
for watches, cursors (direct access to nested values), undo/redo history.
Installation
yarn add @thi.ng/atom
Usage examples
A complete minimal webapp example is in the
/examples/todo-list
directory.
Live demo here
Atom
import * as atom from "@thi.ng/atom";
const a = new atom.Atom(23);
a.deref();
a.addWatch("foo", (id, prev, curr) => console.log(`${id}: ${prev} -> ${curr}`));
a.swap((val)=> val + 1);
a.reset(42);
Cursor
Cursors provide direct & immutable access to a nested value within a structured
atom. The path to the desired value must be provided when the cursor is
created. The path is then compiled into a getter and setter to
allow cursors to be used like atoms and update the parent state in an immutable
manner (i.e. producing an optimized copy with structural sharing of the
original (as much as possible)) - see further details below.
It's important to remember that cursors also cause their parent state (atom or
another cursor) to reflect their updated local state. I.e. any change to a
cursor's value propagates up the hierarchy of parent states.
a = new atom.Atom({a: {b: {c: 1}}})
b=new atom.Cursor(a, "a.b")
c=new atom.Cursor(b, "c")
c.reset(2);
b.deref();
a.deref();
For that reason, it's recommended to design the overall data layout rather wide
than deep (my personal limit is 3-4 levels) to minimize the length of the
propagation chain and maximize structural sharing.
main = new atom.Atom({ a: { b: { c: 23 }, d: { e: 42 } }, f: 66 });
cursor = new atom.Cursor(main, "a.b.c");
cursor = new atom.Cursor(main, ["a","b","c"]);
cursor = new atom.Cursor(
main,
(s) => s.a.b.c,
(s, x) => ({...s, a: {...s.a, b: {...s.a.b, c: x}}})
);
cursor.addWatch("foo", console.log);
cursor.deref()
cursor.swap(x => x + 1);
main.deref()
Undo history
db = new atom.History(new atom.Atom({a: 1}))
db.deref()
db.reset({a: 2, b: 3})
db.reset({b: 4})
db.undo()
db.undo()
db.undo()
db.canUndo()
db.redo()
db.redo()
db.redo()
db.canRedo()
Getters & setters
The getter()
and setter()
functions transform a path like a.b.c
into a
function operating directly at the value the path points to in nested object.
For getters, this essentially compiles to val = obj.a.b.c
.
The resulting setter function too accepts a single object to operate on and
when called, immutably replaces the value at the given path, i.e. produces
a selective deep copy of obj up until given path. If any intermediate key is
not present in the given object, it creates a plain empty object for that
missing key and descends further along the path.
s = setter("a.b.c");
s = setter(["a","b","c"]);
s({a: {b: {c: 23}}}, 24)
s({x: 23}, 24)
s(null, 24)
In addition to these higher-order functions, the module also provides
immediate-use wrappers: getIn()
, setIn()
, updateIn()
and deleteIn()
.
These functions are using getter
/ setter
internally, so have same
behaviors.
state = {a: {b: {c: 23}}};
getIn(state, "a.b.c")
setIn(state, "a.b.c", 24)
updateIn(state, "a.b.c", x => x + 1)
deleteIn(state, "a.b.c.")
Only keys in the path will be modified, all other keys present in the given
object retain their original values to provide efficient structural sharing /
re-use. This is the same behavior as in Clojure's immutable maps or those
provided by ImmutableJS (albeit those implementation are completely different -
they're using trees, we're using the ES6 spread op and recursive functional
composition to produce the setter/updater).
s = setter("a.b.c");
a = { x: { y: { z: 1 } }, u: { v: 2 } };
b = s(a, 3);
a.x === b.x
a.x.y === b.x.y
a.u === b.u;
Authors
License
© 2018 Karsten Schmidt // Apache Software License 2.0