Actions
Actions are functions attached to the .do
property of the Leaf.
You may notice that there is little difference between an action and an external function. you would be eventually correct.
function updatePoint(point, x, y) {
point.do.set_x(x);
point.do.set_y(y);
}
const point = new Forest({$value: {x: 0, y: 0},
actions: {
update: updatePoint
}});
point.do.update(1, 2);
console.log('update (internally) to {x: 1, y: 2} ', point.value);
updatePoint(point, 3, 4);
console.log('update (externally) to {x: 3, y: 4}', point.value);
console.log('------ started to watch via subscribe() --------');
point.subscribe((value) => console.log('subscribed value: ', value));
point.do.update(10, 20);
console.log('update (internally) to {x: 10, y: 20} ', point.value);
updatePoint(point, 30, 40);
console.log('update (externally) to {x: 30, y: 40}', point.value);
/*
update (internally) to {x: 1, y: 2} { x: 1, y: 2 }
update (externally) to {x: 3, y: 4} { x: 3, y: 4 }
------ started to watch via subscribe() --------
subscribed value: { x: 3, y: 4 }
subscribed value: { x: 10, y: 20 }
update (internally) to {x: 10, y: 20} { x: 10, y: 20 }
subscribed value: { x: 30, y: 20 } <<< mid-action noise
subscribed value: { x: 30, y: 40 }
update (externally) to {x: 30, y: 40} { x: 30, y: 40 }
*/
However, code that occurs in an embedded action is quieter and transitionally guarded; either all the changes are committed, or none of them are. And subscribers are not notified of those changes until all of them occur and are validated, and committed.
This is one of those "nuance" differences that you may be indifferent to until your actions start throwing errors.
Action Parameters
The action you write in the config file will always be passed the proper child as the first argument. When calling the action you do NOT (and should not) pass that leaf in yourself - it will be done automatically.
Any further parameters you want can be passed in after that.
const count = new Forest({
$value: { count: 0 },
children: {
increment: {
$value: 1,
type: true,
test: (value) => {
if (value === 0) {
return 'increment cannot be set to zero'
}
return false
}
}
},
actions: {
inc(leaf, incr = undefined) {
if (incr !== undefined) {
leaf.do.set_increment(incr)
}
leaf.do.set_count(leaf.value.count + leaf.value.increment)
return leaf.value.count
}
}
})
count.subscribe(console.log)
count.do.inc()
count.do.inc(3)
count.do.inc()
count.do.inc()
try {
// DO NOT pass leaf in as the first argument manually -- this is an error.
count.do.inc(count, 2)
} catch (err) {
console.log('parameter error:', err.message)
}
count.do.inc(2)
count.do.inc()
/*
{ count: 0, increment: 1 }
{ count: 1, increment: 1 }
{ count: 4, increment: 3 }
{ count: 7, increment: 3 }
{ count: 10, increment: 3 }
parameter error: cannot add value of type object to leaf root:increment:11f7e64b-5877-4615-87df-a151a08f2887 (type number)
{ count: 12, increment: 2 }
{ count: 14, increment: 2 }
*/
increment -- the optional second argument -- is passed into a setter action (see below); and if that fails, the value is not incremented, nor is the bad value accepted. If you stupidly try to assert your Forest or a leaf as a parameter, it will be taken as the second parameter -- not likely the intended result.
You can, of course, write typescript checks into your functions -- however, they may not be completely transparent to the calling context.
Return values
Leaf actions are not required to have a return value. But they can. If the only point of an action is to derive a value from the forest's current state, consider using selectors instead.
Actions that are async will return a promise, as shown below.
Async actions
Actions that are asynchronous will not receive transactional insulation -- because promises return indefinitely, its just too messy to roll back state in the event of an asynchronous error.
Dynamic set
methods
There is a method that works for all container type: myLeaf.set(keyName, value)
. Any sort of key can be used in this
manner: strings and symbols for objects, numbers for arrays, and literally anything for a map.
set can add new keys, or overwrite old ones' value. It delegates to setting the child if a child has been mapped to the key.
set(key, value)
will trigger validation, and throw if the value doesn't meet the type of the child or causes the target
Leaf instance to fail its test(s). It is, in fact, what the .do.set_keyname(value)
methods call, ultimately.
It throws if the Leaf's value is not in the container
family (Object, Map, Set, Array).
Automatic key setters
Why snake case for setters?
Version 1.0 of Forest (and LookingGlass Engine from which it was derived) used snakeCase for setters. However,
that created some difficult scenarios; for instance, if derived from a variable with different case variables such as
{x: 1, X: 10}
, what would myForest.do.setX(100)
change? Hard to say. So, with snake case, you aren't obligated
to mutate the name of the key suffix of the method allowing for set_x
and set_X
to coexist peacefully.
Along with the actions you define (if any) are "setter" actions, that are automatically generated on creation
of the leaf. Setters are named set_(keyname)
based on the key of the property to change. While objects usually have
names with constraints similar to javascript variables. As keyname
is the name of a key of a JavaScript object
-- OR the key of a JavaScript Map. SO technically Any string can be used for the keyname
, and therefore a setter
might end up being set_a very bad variable name 3.14159...
.
Setters are created based on the initial keys of the leaf's value, as well as the keys of any children; as long as those keys are strings. This means these setters will persist even if the key they update is not presently in the value, and keys that are added to the value will not be provided setters.
Both use cases can cause some odd names if the keys are oddly named; object keys, and map keys, can have spaces, symbols,
etc. in them. You can't of course call myForest.do.set_a very bad variable name 3.14159...(100)
.
However, you can if you want callmyForest.do['set_a very bad variable name 3.14159...'](100)
-- i.e., with array access.
Refreshing Setters
If you update the value to have new keys, the setters will not change unless you call leafInstance.updateDo(true)
.
After each call of updateDo, the setters will be based on the union of the current value including the child keys.
This may erase some setters if the keys of the value have been altered.
fixedSetters
There are times when you want to lock in the setters; if you have a more rigid API
and/or you want setters for optional properties that may or may not be present (yet),
you can set the .fixedSetters
property of the Leaf instance (or set it in its configuration)
to an array, to freeze the keys for which setters actions are created.
This will even cause the setters to ignore the keys of the value, and those of children.
Once fixedSetters is defined, the setters only reflect its values.
Having fixed set keys will override setters not only for the value keys but for any children.
If you have fixedSetters defined, then updateDo()
will not really do anything (regarding the setters);
they will always be derived exclusively from the fixedSetters, regardless of the value of the leaf, or any children present.