Skip to content
This repository has been archived by the owner on May 31, 2020. It is now read-only.

xamfoo/reactive-obj

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

reactive-obj

Meteor reactivity for nested objects.

Build Status

Contents

Install

meteor add xamfoo:reactive-obj

Dependencies

  • underscore
  • tracker

Getting Started

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}

Demo

Usage

new ReactiveObj([initialValue], [options])

Constructor for a single reactive object.

  • initialValue Object
    • Initial value to set. If no value is provided, it defaults to an empty object.
  • options Object
    • transform Function
      • Specify a transform function for all values returned via get() and update(). transform should take a single argument value and return the new value.

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 1

reactiveObj.get([keyPath])

or 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.

  • keyPath String 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.
  • valueIfNotSet Any (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 2

reactiveObj.equals([keyPath], value)

Returns 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.

  • keyPath String or Array of String
  • value Any

reactiveObj.set([keyPath], value)

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.

  • keyPath String or Array of String
  • value Any
    • 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}

reactiveObj.setDefault([keyPath], valueIfNotSet)

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.

  • keyPath String or Array of String
  • valueIfNotSet Any

Example:

var x = new ReactiveObj({a: 20});
x.setDefault('a', 1)
.setDefault('b', 2);
x.get(); // Returns {a: 20, b: 2}

reactiveObj.update(keyPath, [valueIfNotSet], updater)

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.

  • keyPath String or Array of String
  • valueIfNotSet Any (default=undefined)
  • updater Function

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 1

reactiveObj.forceInvalidate(keyPath, [options])

Invalidate 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.

  • keyPath String or Array of String
  • options Object
    • allTypes Boolean (default=false)
      • Setting this true will invalidate reactive dependents for any type of values, effectively causing all dependents on the value and its children to re-run.
    • noChildren Boolean (default=false)
      • Set this to true to ignore children dependents, so that only dependents matching the keypath will be invalidated.

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 2

Experimental Features

reactiveObj[ArrayMethod](keyPath, methodArgs...)

Applys 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.

  • ArrayMethod String
    • Supported methods: push, pop, reverse, shift, sort, splice, unshift
  • keyPath String or Array of String
  • methodArgs Any
    • 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']

reactiveObj.select(keyPath)

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.

  • keyPath String 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']}}

Discussion

Why use this instead of Session, ReactiveVar or ReactiveDict?

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,

  • equalsFunc usually 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.

Why doesn't get and update return cloned objects by default?

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.