boxed-immutable
Immutable proxy wrapper with auto-vivification of intermediate objects/arrays with syntactic
sugar to keep access/modification to deeply nested properties looking almost the same as plain
object property access.
Create a boxed-immutable object proxy then access and/or modify its nested properties, ignoring
whether intermediate values are objects/arrays or whether they exist.
Original object/array are shallow copied on first modification (all the way back to the root
collection), thereafter all mods are done on the copy. This occurs at every level so minimal
copying is done on all modifications and any unmodified values are re-used.
Subsequent modifications will be done on a copy. Once a copy is made it is re-used for the life
of the boxed object since it is detached from the original.
Access to object's original properties occurs in two ways:
- Use original property name to get access to the raw underlying object's property value.
Trying to access a field or array index when the property is not valid will throw
TypeError
or ReferenceError
. - Use original property name with
_$
appended to the end of it to get access to a proxy which
will auto-vivify the property container when the first property is set.
Use option 1 to access leaf values in the object since JavaScript handles this nicely.
Use option 2 to access intermediate properties.
Easily customize the suffix/prefix combination that will not conflict with property names in
your project's state objects.
Install
Use npm to install.
npm install boxed-immutable --save
Usage
const _$ = require('boxed-immutable')._$;
function updateState(confirmationName, confirmationValue) {
let state = _$(this.getState());
state.fieldStatus = 5;
const confirmationData = state.properties_$.confirmations_$[confirmationName + "_$"];
confirmationData.count = 1 + (confirmationData.count || 0);
confirmationData.value = confirmationValue;
this.setState(state.delta$);
this.setState(state.deepDelta$);
}
Use it to safely access deeply nested values without throwing TypeError
:
let boxed = _$(someValue);
let caption = boxed.appState_$.dashboard_$[dashboardName + "_$"].captionText || 'default caption';
let boxed = _$(undefined);
let caption = boxed.appState_$.dashboard_$[dashboardName + "_$"].captionText || 'default caption';
Auto-Vivication of intermediate properties
When setting properties on non-existent or non-array or object properties the proxy will
auto-create the array or object, ie. auto-vivify it.
Auto-vivication takes the property name into account: integers whether numbers or numbers in
strings will create an Array
, everything else will create an Object
.
You can use the special end of array
value of "_$"
or just the global box _$
or ._$
property on any boxed value when setting an array property to have the value added to the end of
the array. Effectively, _$
is always equal to the length of the array.
let empty = _$();
empty[""] = 5;
empty["_$"] = 5;
empty[_$] = 5;
empty._$ = 5;
empty._$ = 10;
empty._$ = 20;
let result = empty.unboxed$;
The following will create an object:
let empty = _$();
empty["field"] = 5;
empty.field = 5;
empty._$ = 10;
empty._$ = 20;
let result = empty.unboxed$;
For symmetry, you can also use the end of array index _$
on objects. In case of objects _$
is equal to the greatest integer key in the object or 0 if no integer keys. The goal is to make
boxed values allow setting properties like their unboxed JavaScript counterparts.
let obj = {};
obj.prop = "a";
obj[0] = 5;
obj[1] = 15;
obj[2] = 25;
let result = obj.unboxed$;
let obj = _$({});
obj.prop = "a";
obj._$ = 5;
obj._$ = 15;
obj._$ = 25;
Options
You can change a couple of options on how the boxing handles properties whose value is
undefined
and modify the prefix/suffix used for accessing boxed properties or magic properties:
Use the createBox(options)
function from the module to create a boxing function with
non-default options:
const createBox = require('boxed-immutable').createBox;
const $__$ = createBox({prefixChars: "$_", suffixChars:"_$", magicPrefixChars: "", magicSuffixChars: "$$"});
let obj = $__$();
obj.$_field_$.subField = 4;
obj.$_prop_$.$__$ = "a";
obj.$_prop_$.$__$ = "b";
obj.$_prop_$.$__$ = "c";
let result = obj.unboxed$$;
Option | Default | Description |
---|
deleteEmptyCollections : | true | if deleting a property results in an empty collection, delete that too |
ignoreUndefinedProperties : | true | when copying delta and deepDelta ignore properties with undefined value |
arrayDeltaObjects : | false | return array delta as objects with index as key |
arrayDeltaObjectMarker : | undefined | field name to set when returning array delta as objects, undefined means don't set |
arrayDeltaObjectMarkerValue : | undefined | value for above |
arrayDeltaPartials : | false | return array delta as partials, any unset indices will be undefined |
arrayDeepDeltaObjects : | false | return array deepDelta as objects with index as key |
arrayDeepDeltaObjectMarker : | undefined | field name to set when returning array deepDelta as objects, undefined means don't set |
arrayDeepDeltaObjectMarkerValue : | undefined | value for above |
arrayDeepDeltaPartials : | true | return array deepDelta as partials, any unset indices will be undefined , false will return array delta from index 0 to last modified index |
prefixChars : [1] | "" | prefix for boxed properties. |
suffixChars : [1] | "_$" | suffix for boxed properties. |
magicPrefixChars : [1] | "" | prefix for magic properties, applied after prefixChars |
magicSuffixChars : [1] | "$" | suffix for magic properties, applied before suffixChars |
* [1]: Note use of [_$]
is not affected by prefixes or suffixes since the function is passed
as property, use of ["_$"]
and ._$
has to have the prefix/suffix used in the options for the
box context
API
boxed-immutable box
Change _$
to your combination of prefix/suffix if modifying defaults.
Magic properties except _$
are wrapped in magicPrefixChars
and magicSuffixChars
,
properties and empty string ""
which represents array end are wrapped in prefixChars
and
suffixChars
Magic properties are accessible with or without the property wrapper (prefixChars
and
suffixChars
) for some it affect whether they provide/return regular values or boxed proxies
for these values.
Where it makes a difference, both wrapped and unwrapped magic properties are given.
Magic Properties of boxed object shown with defaults for wrapping prefix/suffix options:
Property | Get | Set | Delete | Call |
---|
_$ | proxy for the boxed object | append end of array | error | does a call on first argument, use: boxed._$(_$ => { }); returns boxed |
get$ | function | error | error | .get$("prop") is same as ["prop" + "_$"] , convenience function when you have a property name in a variable and need a boxed version of it |
forEachKey$ | function | error | error | functions executes callback for each property key .forEach$((prop, unboxedValue) =>{}); skip undefined values, addtionally for arrays prop is integers >=0 |
forEachKey$_$ | function | error | error | functions executes callback for each property key .forEach$((prop, boxedValue, unboxedValue) =>{}); skip undefined values, addtionally for arrays prop is integers >=0 |
unboxed$ | unboxed value | set value of boxed property and mark as modified | delete property in parent | error |
modified$ | value if modified else undefined | same as above | same as above | error |
default$ | function | set value if it is undefined, otherwise do nothing | error | error |
boolean$ | value converted to true or false | convert passed value to true or false before setting | error | error |
delta$ | modified properties of first level, full props thereafter: shallow delta | do shallow delta update of properties, all properties after first level will be changed | error | error |
deepDelta$ | modified properties only of all levels: deep delta | do deep delta update with value, only modified properties of all levels are changed. | error | error |
Use of ._$()
, sometimes you need to modify deep properties based on programming logic. Instead
of creating an object then adding it to your modified state, you can use this option and benefit
from not worrying about immutability:
let boxed = _$(someState);
boxed.appState_$.dashboards_$.userData_$(_$ => {
if (condition) {
_$.undoList_$.modified_$(_$ => {
});
}
});
:warning: When a value is set on the parent collection it orphans the boxed state for all the
properties of the parent for which you kept reference. These detached properties will still work
but only on their own copy of the data since they are now detached from the root.
For example this will happen when you do something like:
let boxed = _$();
let nested = boxed.level1_$.level2_$.level3_$;
nested._$ = 0;
nested._$ = 1;
nested._$ = 2;
// boxed is now: { level1: { level2: { level3: [0,1,2]}}};
boxed.level1_$.level2_$.level3 = [0,1,2]; // this will detach all boxed properties from level3 and below, like nested
nested._$ = 3;
nested._$ = 4;
nested._$ = 5;
// nested is [0,1,2,3,4,5]
// boxed is still: { level1: { level2: { level3: [0,1,2]}}};
boxed-immutable boxOnDemand
Provides a boxed proxy to immutable state, with .save()
and .cancel()
methods for saving or
canceling state changes. With minimal code this allows transparent access to current state
without the callers worrying about stale data or how to apply the changes back to the state
holder.
Applying partial changes to component's state is as easy as setting a value in boxedOnDemand
instance and invoking .save()
const boxOnDemand = require('boxed-immutable').boxOnDemand;
function getSimpleState() {
}
let stateHolder;
function saveState(newState) {
stateHolder.cancel();
}
stateHolder = boxOnDemand(undefined, () => {
return getSimpleState();
}, (modified, boxed) => {
saveState(modified);
});
function getState() {
return stateHolder;
}
let state = getState();
state.save();
state.cancel();
Property | Get | Set | Delete | Call |
---|
save | function | error | error | calls the saveState callback passed to boxOnDemand function, returns value returned from callback, callback only called if there were changes made to boxed object |
cancel | function | error | error | cancels any changes and destroys the boxed object, it is recreated on next access with a fresh copy of the immutable state, returns proxy this for chaining calls |
boxOnDemand(getState, saveState, options)
const boxOnDemand = require('boxed-immutable').boxOnDemand;
const onDemandState = boxOnDemand();
Used to construct a new boxed on demand proxy.
argument | default | Description |
---|
getState | none | callback to call to obtain the current state |
saveState | none | callback to call on save operation, returned result passed back to caller of save() . |
options | box | options to use. Can be a box as provided by boxedImmutable.box or boxedImmutable.createBox(), then all other options are set to defaults |
Option | Default | Description |
---|
box : | global box | Which box creation to use for each new boxed state object |
saveBoxedProp : | 'save' | name of the save property to use, allows changing of the function to 'commit' or something that will not conflict with your state's properties |
cancelBoxedProp : | 'cancel' | name of the cancel property to use, allows changing of the function to something that will not conflict with your state's properties |
wrapProps : | false | if true will wrap the saveBoxedProp and cancelBoxedProp the same as magical properties of the boxed object created from the box |
License
MIT, see LICENSE.md for
details.