Skip to content

Instantly share code, notes, and snippets.

@evanrmurphy
Last active August 29, 2015 13:58
Show Gist options
  • Select an option

  • Save evanrmurphy/ced9358cff14ea3ae2a8 to your computer and use it in GitHub Desktop.

Select an option

Save evanrmurphy/ced9358cff14ea3ae2a8 to your computer and use it in GitHub Desktop.
Computed observable arrays for Knockout
define([
'underscore',
'knockout'
], function(_, ko) {
// Example usage:
//
// function alphabeticalComputeFn = function(metadataOA) {
// function getName(metadatum) { return metadatum.name(); }
// return _.sortBy(metadataOA(), getName);
// });
//
// var metadataOA = metadataCollection.accessor()._instances;
// var alphabeticalMetadataCOA =
// computedObservableArray(metadataOA, alphabeticalComputeFn);
function computedObservableArray(OA, computeFn) {
var persistingCOA = ko.observableArray();
return ko.computed(function() {
var newCOA = computeFn(OA);
mergeObservableArrays(persistingCOA, newCOA);
return persistingCOA;
});
}
function mergeObservableArrays(destinationOA, sourceOA) {
var itemsToRemove = _.difference(destinationOA(), sourceOA());
var itemsToAdd = _.difference(sourceOA(), destinationOA());
function sourceOAIndex(item) { return _.indexOf(sourceOA(), item); }
var indexedItemsToAdd = _.indexBy(itemsToAdd, sourceOAIndex);
function removeItem(item) { destinationOA.remove(item); }
_.each(itemsToRemove, removeItem);
function addItem(item, index) { destinationOA.splice(index, 1, item); }
_.each(indexedItemsToAdd, addItem);
}
});
@aaronj1335
Copy link

so there's this pretty big motivating use case that this doesn't support. consider a set of subscribers:

[
  {id: '123', msisdn: '0123456789'},
  {id: '456', msisdn: '2345678901'},
  // ...

and we've got a SubscriberCollection with instances of SubscriberModels that have an observable for the id and msisdn.

so then we'll want a table of these somewhere, and that view is going to want to display pretty phone numbers. we want a computed for a nicely formatted number, but that's a view concern, so we don't want to stick that on the model (in reality this is a useful SubscriberModel property, but there will be more specific, view-only cases of stuff like this, so just humor me here).

what we want is to bind the table rows to something like this:

var rowsModel = ko.computed(function() {
  return _.map(subscribersCollection(), function(subscriberModel) {
    return _.extend({
      phoneNo: ko.computed(prettFormatMSISDN);
    }, subscriberModel);
  });
});

this is pretty much what you've got (except it's only for mapping, not sorting/filtering). it's nice because:

  • it separates view stuff from app-wide data concerns
  • if something is added to subscribersCollection() or one of the models is updated, the UI is updated

the problem is it re-renders the entire table every time subscribersCollection() changes. this is a perf problem (we encountered it on the stream), but more importantly if those rows contain any view state (say an input on each row that allows you to make updates inline), then those are blown away by just adding a row. this is the difference between using ko's foreach binding w/ an OA and a POJO array.

does this make sense? if we standardize a COA implementation, it needs to cover this case (we've already got 2 examples of it in the code).

also note that if we do cover this case, we need some way of determining if an item in the mapped array corresponds to one in the original. in your example we can just check if objects equal eachother, in mine we'd need to compare .id()s, and in the existing examples in the code we assume the .data() field of the mapped array contains the original object, so we just test the equality of that.

effin complicated, i kno...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment