Skip to content

Recursively tracks changes within a view model no matter how deeply nested the observables are or whether they are nested within dynamically created array elements.

License

Notifications You must be signed in to change notification settings

ZiadJ/knockoutjs-reactor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

KO-Reactor

A KnockoutJS plugin that lets you track all changes within a view model seamlessly with the ability to pin point and process them on the fly. It does not require any modifications to the markup or view model itself which makes it ideal for testing and quick prototyping.

Usage:

ko.watch(targetObjectOrFunction, options, evaluatorCallback, context);

or

var property = ko.observable().watch(targetObjectOrFunction, options, evaluatorCallback, context);

targetObjectOrFunction accepts any subscribable or function/object containing the targeted subscribables.
options is only optional and can be replaced by evaluatorCallback the response and evaluation function.

Deep Watcher:
The depth option is used to limit the recursion depth. Its default value 1 limits it to a flat view model but a value of -1, as shown below, will fully unleash it:

var viewModel = { ... };
ko.watch(viewModel, { depth: -1 }, function(parents, child, item) {
	...
});

The callback function provides three arguments:

  1. parents: A list of all parents above the changed property.
  2. child: The child property that changed.
  3. item: Used to determine whether an array item has been added, moved or removed.

Note that for observable arrays, subscriptions are created or destroyed on the fly as new items are added or removed. To keep up with changes in the model structure itself however the mutable option needs to be set to true.

Auto Wrapper:
Setting the wrap option to true will make sure that all fields within the targeted object are wrapped within an observable even if new array items are added later on.

Chaining Support:
The chaining support provided adds in more ways to simplify the code and make it more readable. For example let's consider this simple view model:

var params = {
    p1 = ko.observable(),
    p2 = ko.observable(),
    p3 = ko.observable()
}

To update a variable, say items, whenever any parameter within params changes we would most probably write something along those lines:

var items = ko.observableArray();      	 	        var items = ko.computed(function() {
ko.computed = function() {             	    	        var p1 = self.params.p1();
    var p1 = self.params.p1();         	    	        var p2 = self.params.p2();
    var p2 = self.params.p2();         	OR	            var p3 = self.params.p3();
    var p3 = self.params.p3();         	         
														var newItems = ...;
    var newItems = ...;                     	        return newItems;
    self.items(newItems);               	        }
}                                       

However, using the plugin, we can just watch params right away like so and let it do the rest:

var items = ko.observableArray();         	    	var items = ko.observableArray();
ko.watch(params, function() {              		    items.watch(params, function() {    
    var newItems = ...;                 OR   	    	var newItems = ...;                 
    self.items(newItems);                   	        return newItems;                    
});                                       	      	}); 

Finally adding a bit of fluency to the mix we end up with the single block below:

var items = ko.observableArray().watch(params, function() {
    var newItems = ...;
    return newItems;    
});

Note that by omitting the target parameter we obtain a chainable version of .subscribe. So instead of creating a subscription separately like so:

var someProperty = ko.observable();
someProperty.subscribe(someFunction, options);

We can do away with the redundancy like so:

var someProperty = ko.observable().watch(someFunction, options);

Selective Subscription:
There are many ways we can go about making a selective watcher. Suppose we want to avoid watching the variable p3. To achieve this we could move p3 to another level like so:

var params = {
    p1: ko.observable(),
    p2: ko.observable(),
    ignored: {
        p3: ko.observable() 
    }
};

But this assumes that the depth option is set to 1. To avoid this we can pass in a new object made up of p1 and p2 only:

ko.watch({ 1: params.p1, 2: params.p2 }, function (parents, child, item) {
    ...
}

Or we can just use the hide option this way:

ko.watch(this.params, { hide: params.p3 }, function (parents, child, item) {
    ...
}

The easiest way however is to simply tag p3 as non-watchable like so:

var params = {
    p1: ko.observable(),
    p2: ko.observable(),
    p3: ko.observable().watch(false) 
};

For more advanced cases however we can make use of the beforeWatch option which gives us full control over the subscription process:

ko.watch(this.params, {
    beforeWatch: function (parents, child) {
        if (<some reason not to subscribe>)
            return false; // cancel subscription
    }
}, function (parents, child, item) {
	...
});

Pausable Listener:
Pausing and resuming a reactor on any property can be done like so:

this.data.watch(false);
//...do work
this.data.watch(true);

Dispose/Unwatch:
The watch function returns an object with a "dispose" method you can call to dispose the watch action:

var viewModel = { ... };
var res = ko.watch(viewModel, { depth: -1 }, function(parents, child, item) {
    ...
});

res.dispose();

Once disposed your model will be "unwatched"

Synchronous tracking: (since 1.4.0)
By default when new objects are added to the tree, they are automatically "watched" asynchronously (e.g: in a setTimeout) in order to keep the system more responsive.

Sometimes this behaviour is not "expected", so you can use the async: false option to force watch to happen "inline".

Single notification for multiple array changes vs change by change notifications (since 1.4.0)
By default when items are moved in an array you receive 2 different notifications, the first will report the "deleted item" and the second one will report the "added item".

If you prefer to receive an array of items in a single notification you can use the splitArrayChanges: false option. In that case you will receive an array of item instead of a single item even when there is only one item changed.

Projects using KO-Reactor:
As of now I'm only aware of this excellent undo manager by Stefano Bagnara:

https://github.com/bago/knockout-undomanager

Here's a Plunker showing it in action on an array within an array:

http://plnkr.co/edit/nB2129?p=preview

Reactor is also used (via Undomanager) in Mosaico the first opensource Email Template Editor:

https://github.com/voidlabs/mosaico

Do feel free to let me know if you have anything related to share and I'll gladly mention it here.

Further Notes:
Unlike ko.computed no listeners are created for subcribables within the evaluator function. So we no longer have to be concerned about creating unintended triggers as the code gets more complex.

Yet another usage not mentioned above example involves calling watch on a function as shown below:

ko.watch(function() { ... }, options);

However it is nothing more than a cosmetic enhancement since it only returns a computed of the function being passed in.

For an extensive list of available options please consult the param sections of the source code.

Development

In order to build and minify you can simply call grunt:

grunt

If you want a live rebuild for each changed

grunt develop

If you want to run the test suite:

grunt jasmine

If you want to generate the test runner in order to run the test suite in your browser

grunt jasmine:main:build

About

Recursively tracks changes within a view model no matter how deeply nested the observables are or whether they are nested within dynamically created array elements.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published