can-derive is a plugin that creates observable filtered lists that remain in sync with their original 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 list regardless of 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).
Check out the demo.
Use npm to install can-derive
:
npm install can-derive --save
Use require
in Node/Browserify workflows to import the can-derive
plugin
like:
require('can');
require('can-derive');
Use define
, require
, or import
in StealJS workflows
to import the can-derive
plugin like:
import 'can';
import 'can-derive';
Once you've imported can-derive
into your project 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 can.List
of todo
objects:
var todos = new can.List([
{ name: 'Hop', complete: true },
{ name: 'Skip', complete: false },
//...
]);
var completed = todos.dFilter(function(todo) {
return todo.attr("complete") === true;
});
Any changes to todos
will be propagated to the derived completed
list:
completed.bind('add', function(ev, newItems) {
console.log(newItems.length, 'item(s) added');
});
todos.push({ name: 'Jump', complete: true },
{ name: 'Sleep', complete: false }); //-> "1 item(s) added"
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()
.
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
which stores its values in a Red-black tree
for performance - rather than a series of numeric keys.
sourceList.filter(predicateFn) -> FilteredList
Similar to .filter()
except
that the returned FilteredList
is bound to sourceList
.
Returns a FilteredList
.
Since FilteredList
inherits from can.RBTreeList,
the following methods are available:
.attr()
.each()
.eachNode()
.filter()
.indexOf()
.indexOfNode()
.map()
.slice()
(coming soon)
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()
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
modeled after an
Array
- 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).
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.
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