Meteor reactivity for nested objects.
meteor add xamfoo:reactive-obj
underscoretracker
var state = new ReactiveObj({a: {b: {c: 1}}});
var print = Tracker.autorun(function () {
console.log( state.get(['a', 'b']) ); // Prints {c: 1}
});
state.set(['a', 'x'], 2); // Prints nothing
state.set(['a', 'b', 'c'], 42); // Prints {c: 42}
state.get('a'); // Returns {b: {c: 42}, x: 2}- Simple example - Demonstrates reactivity on nested properties (source)
Constructor for a single reactive object.
initialValueObject- Initial value to set. If no value is provided, it defaults to an empty object.
optionsObjecttransformFunction- Specify a transform function for all values returned via
get()andupdate().transformshould take a single argument value and return the new value.
- Specify a transform function for all values returned via
Example of a transform function:
var state = new ReactiveObj({}, {
transform: function (value) {
return EJSON.clone(value); // cloning prevents changes to the original value
}
});
state.set('a', {x: 1});
state.get('a').x = 2;
state.get(['a', 'x']); // Returns 1or reactiveObj.get(keyPath, [valueIfNotSet])
Returns the object's property specified by the keypath, or valueIfNotSet if
the key does not exist. Establishes a reactive dependency on the property.
keyPathString or Array of String- Pointer to a property of an object. If not specified, this returns the top
level object.
['fruits', 'apple']and'fruits.apple'are equivalent and valid keypaths.
- Pointer to a property of an object. If not specified, this returns the top
level object.
valueIfNotSetAny (default=undefined)
Beware of mutating the returned value as it changes the stored object without
triggering reactivity. A way to avoid this is to specify a transform
function that clones the object in the constructor options.
If this method returns undefined when valueIfNotSet is not provided, that
does not guarantee the key does not exist because the key could be associated
with an undefined value.
Example:
var x = new ReactiveObj({a: 1, b: [10, 20]});
x.get('a'); // Returns 1
x.get(['b', 'c']); // Returns undefined
x.get('b.1'); // Returns 20
x.get('c', 2); // Returns 2Returns true if the object's property specified by the keypath is equals to
the value or false if otherwise. Establishes a reactive dependency which is
invalidated only when the property changes to and from the value.
keyPathString or Array of StringvalueAny
Replaces the object's property specified by the keypath and returns the
reactiveObj that can be used for chaining. Properties that do not exist will
be created.
keyPathString or Array of StringvalueAny- Value to set
To replace the root node, use [] as the keypath.
Example:
var x = new ReactiveObj;
x.set(['a', 'b'], 1);
x.get('a'); // Returns {b: 1}Sets the object's property specified by the keypath if it hasn't been set
before and returns the reactiveObj that can be used for chaining. Keys in
the keypath that do not exist will be created.
keyPathString or Array of StringvalueIfNotSetAny
Example:
var x = new ReactiveObj({a: 20});
x.setDefault('a', 1)
.setDefault('b', 2);
x.get(); // Returns {a: 20, b: 2}Update the value at this keypath with the return value of calling updater.
updater is called with its value or valueIfNotSet if the key was not set.
keyPathString or Array of StringvalueIfNotSetAny (default=undefined)updaterFunction
Beware of mutating the returned value as it changes the stored object without
triggering reactivity. A way to avoid this is to specify a transform
function that clones the object in the constructor options.
Example:
var x = new ReactiveObj({a: 1});
var inc = function (v) { return v + 1; };
x.update('a', inc);
x.update('b', 0, inc);
x.get('a'); // Returns 2
x.get('b'); // Returns 1Invalidate reactive dependents on the value specified by the keypath. You will
need to call this if you mutate values returned by get or update directly.
By default, this will only invalidate values which are instances of Object
like arrays, objects and functions. You can override this behavior in
options.
keyPathString or Array of StringoptionsObjectallTypesBoolean (default=false)- Setting this
truewill invalidate reactive dependents for any type of values, effectively causing all dependents on the value and its children to re-run.
- Setting this
noChildrenBoolean (default=false)- Set this to
trueto ignore children dependents, so that only dependents matching the keypath will be invalidated.
- Set this to
Normally one should avoid using forceInvalidate and treat values returned
from get and update as read-only. This makes the application simpler and
more efficient.
Example:
var state = new ReactiveObj({a: 1}});
var print = Tracker.autorun(function () {
console.log( state.get('a') ); // Prints 1
});
state.get().a = 2;
state.get('a'); // Returns 1
state.forceInvalidate(); // Prints 2Applys a native array method on the value specified by the keypath and returns the result. Throws an error if the value is not an array.
ArrayMethodString- Supported methods:
push,pop,reverse,shift,sort,splice,unshift
- Supported methods:
keyPathString or Array of StringmethodArgsAny- Comma separated arguments for passing to the array method
Example:
var state = new ReactiveObj({a: []});
state.push('a', 'foo'); // Returns 1
state.push('a', 'bar'); // Returns 2
state.get('a'); // Returns ['foo', 'bar']Saves the given keyPath and returns a cursor. This provides convenience to
access deep paths repeatedly. API methods of reactiveObj work on a cursor
similarly.
keyPathString or Array of String
Example:
var state = new ReactiveObj({users: {alice: {}}});
var users = state.select('users');
var alice = users.select('alice');
alice.set('messages', []);
alice.push('messages', 'Hello'); // Returns 1
alice.push('messages', 'World'); // Returns 2
alice.get(); // Returns {messages: ['Hello', 'World']}
users.get(); // Returns {alice: {messages: ['Hello', 'World']}}It is quite common to maintain application state and namespaces in deep structures. It is nice to be able to make those structures reactive as well.
While the mentioned packages are good for maintaining reactive keys and values, they are currently not very efficient when dealing with nested structures.
For example if we declare the following ReactiveVar,
new ReactiveVar({
prop1: {
prop11: {
prop111: ..
}
},
prop2: ..,
prop3: {
prop31: ..
},
..
propN: ..
}, equalsFunc);Every time one of the property is changed,
equalsFuncusually needs to check every property. This will take time if there are a lot of nested properties.- Every reactive dependent on the variable is invalidated. This will also take time if there are many reactive dependents.
However ReactiveObj is created with nested objects in mind. So if a property
is changed,
- Only the changed property is checked.
- Only reactive dependents on the changed property is invalidated.
- In addition, all changes and invalidations are batched so that multiple changes on on the same property will only result in a single check and invalidation.
All things said, the Meteor scene changes quickly so let me know if there is a better way.
Not all structures can be deep cloned easily. It is tricky to clone custom
types, functions and structures containing circular references. But if your
structure is simple, specifying a function containing
JSON.parse(JSON.stringify(value)) or EJSON.clone in the transform option
will do the job.