2
votes

I have following ViewModel:

var Order =  function(data) {
  this.fruits = ko.observableArray(ko.utils.arrayMap(data.Fruis, function(item) { return item; }));
  this.vegetables = ko.observableArray(ko.utils.arrayMap(data.Vegetables, function(item) { return item; }));
};

I need to define some sub-properties and sub-observables bound to the specific instance, and some common methods for fruits and vegetables,:

var Items =  function(data, type) {
  var self = this;
  self.type = type;
  self.choice = ko.observable(-1);
  self.choice.select = ko.computed(function(){
    var id = self.choice();
    // do stuff
  });
  self.choice.remove = function() {
    var id = self.choice.peek();
    // do stuff
  };
  self.add = function(code) {
    //do stuff
    self.choice(id);
  };
};

Which is the right way to bind the function containing my set of methods and sub-observables, so that i can use the methods as follows:

orderViewModel.fruits.add("apples");
orderViewModel.fruits.add("bananas");
orderViewModel.fruits.choice(0);
orderViewModel.fruits.choice.remove();

console.log(ko.tpJSON(orderViewModel));
// prints: {fruits: [bananas], vegetables: []};

I think there is no need to use extenders, as the properties and methods aren't generic, and don't need to be common to all observables.

I tried by returning an observable array from my Item function, but i wasn't able to get this to work, as sub-properties and sub-observables have been lost. How can i bind Items to my observable arrays?

1

1 Answers

3
votes

Even though you might not want to create an extender, what you're doing here is extending an observable array...

If you don't want to register an extender, you can create a small helper function to create an observableArray and add some methods and properties to it before you return.

In the example below you can see some example code. Some important advice:

  • If you use this approach, I'd suggest not overwriting the default methods in observableArray. E.g.: remove takes an item by default; you want it to work with an external choice index... It's best to pick a different name so you keep supporting both.
  • If you end up using the extension a lot, it might be worth it to create a clean viewmodel that stores the observable array internally. You can define a toArray method for exporting to a plain array.

var obsCollection = function(initialItems) {
  
  var items = ko.observableArray(initialItems);
  
  items.choice = ko.observable(-1);
  items.add = items.push;
  
  var ogRemove = items.remove.bind(items);
  
  // I'd rename this to "deleteChoice"
  items.remove = function() {
    var index = items.choice();
    ogRemove(items()[index]);
    // Reset choice to -1 here?
  };
  
  
  return items;
};

var fruits = obsCollection(["Apple"]);

log(fruits);
fruits.add("Banana");
fruits.choice(0);
fruits.remove();
log(fruits);
fruits.remove();
fruits.add("Mango");
fruits.add("Lemon");
log(fruits);

function log(d) {
  console.log(JSON.stringify(ko.unwrap(d)));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

Edit to clarify the (lack of) use of this:

Since we don't use the new keyword, there's no real need to use this. Internally, the observableArray creates a new instance, but our only way of referring to this instance is through items. When detaching prototype methods from the array, we need to make sure we call them with the right context, by either bind or .call(items) (or apply).

If you want the code to look like a "class", you can either do: var self = items; and continue with the self keyword, or rewrite it to use the new keyword (last bullet point in my answer).

var myArray = ko.observableArray([1,2,3]);

try {
  // Reference the function without binding `this`:
  var removeFromMyArray = myArray.remove;

  // Internally, the observableArray.prototype.remove method
  // uses `this` to refer to itself. By the time we call it,
  // `this` will refer to `window`, resulting in an error.
  removeFromMyArray(2);
} catch(err) {
  console.log("ERROR:", err.message);
  console.log(myArray());
}


// By binding to the array, we ensure that the function reference
// is always called in the right context.
var boundRemoveFromMyArray = myArray.remove.bind(myArray);
boundRemoveFromMyArray(2);
console.log(myArray());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>