I frequently find that I have an array of objects in JavaScript that I want to
display in a particular order and also have the ability to quickly locate an object
by an ID or a key (and not use the indexOf function). As my recent project is using
Knockout.JS, I decided to throw together a
function that makes having keyed lookups based on an array simple to maintain.
Here’s an example ViewModel definition:
var ViewModel = function () {
this.uniqueNumber = ko.observable();
this.list = ko.observableArray([]);
this.list_by_keys = this.list.asDictionary('id');
};
The definition includes an array, a unique number (more on that a bit later), the
list (to which the code will bind the UI to), and finally the keyed list.
After a few attempts at a good name, I settled for something that I hated the least.
In any case, usage is simple.
After creating the observable array:
this.list = ko.observableArray([]);
The code creates a second field which will contain all of the objects in the
original array, but in a quickly accessible index (thanks to the nature of
JavaScript objects).
this.list_by_keys = this.list.asDictionary('id');
In the preceding line, the asDictionary function (which I’ve added
to the observableArray definition as you’ll see below) is used and passed the string
‘id’. The ‘id’ is the name of the property of the JavaScript object that is later
added to the list that will contain the key (the primary key, although it’s not
checked for duplicates).
As you’ll note below, an instance of the ViewModel is created and bound to the UI.
var vm = new ViewModel();
ko.applyBindings(vm);
$("#btnAdd").on("click", function () {
var id = vm.uniqueNumber.inc()();
vm.list.push({ id: id,
time: new Date().toLocaleTimeString() });
});
With a click of a button (using jQuery syntax), a new sample object containing an
‘id’ and ‘time’ property is added to the master list. When the new object is added,
the asDictionary code is executed. Why? Because of the use of the
computed
function as shown below. Knockout.JS has computed observables which automatically
track dependencies and execute any time that the source property changes. In this
case, it’s tracking the “this” object, which just happens to be the observableArray
(list).
ko.observableArray.fn.asDictionary = function (keyName) {
return ko.computed(function () {
var list = this() || []; // the internal array
var keys = {}; // a place for key/value
ko.utils.arrayForEach(list, function (v) {
if (keyName) { // if there is a key
keys[v[keyName]] = v; // use it
} else {
keys[v] = v;
}
});
return keys;
}, this);
};
The function loops through each of the elements of the array and stores each object
by the key (if provided, otherwise by the value). Unfortunately, because there
are many ways to adjust an array in JavaScript, this isn’t as efficient as I’d like.
Every time something is added to the array, the entire “dictionary” is recreated.
While this isn’t terrible in reasonable cases, it’s still a bit annoying. You could
add a bit of code to disable the rebuilding conditionally though if performance is
going to be a big concern.
I also was experimenting with a unique number generator. It’s really quite dumb, but
I ‘m posting in nonetheless.
ko.observable.fn.inc = function (incExtra) {
incExtra = incExtra || 1;
var current = this() || 0;
current += incExtra;
this(current);
return this;
};
ko.observable.fn.dec = function (decExtra) {
return ko.observable.fn.inc(decExtra || -1);
};
To use it and retrieve the value, call it like this:
var id = vm.uniqueNumber.inc()();
The odd syntax calls the inc (increment) function which returns the original
object (in support of chaining). Then, to get the value, it calls the properties’
getter function (the second set of parentheses). (As I said, it was just
messing around).
The HTML for the data binding looked like this:
<div class="log" data-bind="foreach: list" >
<div class="item">
<span data-bind="text: id" class="id"></span>
<span data-bind="text: time"></span>
</div>
</div>