can-derive
can-derive is a plugin that creates observable filtered lists that stay up-to-date with a source list.
For example, a todo list might contain todo objects with a completed
property.
Traditionally can.List.filter
enables you to create a new can.List
containing only the "completed" todo objects. However, if the source list were
to change in any way - for instance via an "add" or "remove" - the returned
can.List
may become an innaccurate representation of the source list.
The same filtered list of "completed" todo objects created
with can-derive
's can.List.dFilter
would always be an accurate
representation of with the source, no matter how it was manipulated.
can-derive is ideal for cases where the source list contains at least 10 items and is expected to be "changed" frequently (3 or more times).
See it in action on JSBin.
Install
Use npm to install can-derive
:
npm install can-derive --save
Use
Use require
in Node/Browserify workflows to import the can-derive
plugin
like:
require('can-derive');
Use define
, require
, or import
in StealJS workflows
to import the can-derive
plugin like:
import 'can-derive';
Once you've imported can-derive
into your project, simply use
can.List.dFilter
to generate a derived list based on a predicate
function.
The following example derives a list of completed items from a todo list:
var sourceList = new can.List([
{ name: 'Hop', complete: true },
{ name: 'Skip', complete: false },
//...
]);
var completed = sourceList.filter(function(todo) {
return todo.attr("complete") === true;
});
Any changes to sourceList
will automatically update the derived completed
list:
completed.bind('add', function(ev, newItems) {
console.log(newItems.length, 'item(s) added');
});
sourceList.push({ name: 'Jump', complete: true },
{ name: 'Sleep', complete: false }); //-> "1 item(s) added"
With can.Map.define
If you're using the can.Map.define plugin, you can define a derived list like so:
{
define: {
todos: {
Value: can.List
},
completedTodos: {
get: function() {
return this.attr('todos').dFilter(function(todo){
return todo.attr('complete') === true;
});
}
}
}
}
Note: The can-derive
plugin ensures that the define plugin's get
method will
not observe "length" like it would a traditional can.List
when calling .filter()
.
Accessing FilteredList values
Unlike can.List
and Array
, indexes of a FilteredList
cannot be
accessed using bracket notation:
filteredList[1]; //-> undefined
To access a FilteredList
's values, use .attr()
:
filteredList.attr(); //-> ["a", "b", "c"]
filteredList.attr(0); //-> "a"
filteredList.attr(1); //-> "b"
filteredList.attr(2); //-> "c"
filteredList.attr('length'); //-> 3
This is due to the fact that a FilteredList
inherits a can.RBTreeList
and stores its values in a Red-black tree
for performance - rather than a series of numeric keys.
API
can.List
.dFilter()
sourceList.filter(predicateFn) -> FilteredList
Similar to .filter()
except
that the returned FilteredList
is bound to sourceList
.
Returns a FilteredList
.
FilteredList
Inherited can.RBTreeList methods
Since FilteredList
inherits from can.RBTreeList,
the following methods are available:
.attr()
.each()
.eachNode()
.filter()
.indexOf()
.indexOfNode()
.map()
-
.slice()
(coming soon)
Disabled can.RBTreeList methods
A FilteredList
is bound to its source list and manipulted as it changes.
Because of this, it is read-only and the following can.RBTreeList
methods are disabled:
.push()
.pop()
.removeAttr()
.replace()
.shift()
.splice()
.unshift()
Performance
can-derive
optimizes for insertions and removals, completing them in O(log n)
time. This means that changes to the source list will automatically update the
derived list in O(log n)
time, compared to the standard O(n)
time you would
expect in other implementations.
It does this by:
- Keeping the derived list in a Red-black tree
- Listening for additions or removals in the source list
- Listening for predicate function result changes for any item
This algorithm was originally discussed in [this StackExchange thread](http://cs.stackexchange.com/questions/43447/order-preserving-update-of-a -sublist-of-a-list-of-mutable-objects-in-sublinear-t/44502#44502).
When to Use
In general, it is preferable to use can-derive
over alternative approaches
when:
- Your source list contains 10 or more elements
- You need to know how the filtered list changed, for instance when rendering in the DOM.
Contributing
To set up your dev environment:
- Clone and fork this repo.
- Run
npm install
. - Open
list/test.html
in your browser. Everything should pass. - Run
npm test
. Everything should pass. - Run
npm run-script build
. Everything should build ok