knockout.jsmodelviewmodeldurandal-2.0

How to seperate model from viewmodel in knockout?


I have a model with an observableArray of items, which is just fine. E.g.

var arr = ko.observableArray([{name: ko.observable('hello')}]);

My problem is I want to have a separate array of settings for these items that does not belong in the model itself. In this particular case, I want to keep track of the selected records.

If I put the item in the model, it is trivial.

ko.utils.arrayForEach(arr, function(item) { 
 item.selected = ko.observable(false); 
});

But that permanently changes the model, and breaks other places where I want to add new elements.

So what I tried was make a computable of the array, but that doesnt work well.

var selected = ko.computed(function () {
 if (!arr()) return [];
 return ko.utils.arrayMap(arr().list(), function () {
  return ko.observable(false);
 });
});

because it remembers the returned item, so when arr changes, it breaks things in weird ways.

How do I separate these two things, while still keeping them in sync?


Solution

  • If you are using checkboxes, the best way to do this is to bind checkedValue to be the object and bind checked to a separate observableArray in the root. then each item that you select using the checkbox will be in the root array. This is not exactly the same as what you asked, but it is a very easy solution for knowing which item is selected.

    If you must do it the way you specify, you probably have to duplicate the array (or create a reference array with all the keys of the original array) and then use a foreach binding to list the objects and use a click binding to pass the object to a function that will look it up in the reference array and set the selected/unselected property.

    There are other options depending on your UI, so maybe a little more information about that would be helpful.

    EXAMPLE:

    <div class="form-group" data-bind="visible: sites().length">
     <label for="siteSelect" class="form-label col-sm-2"
      Sites
     </label>
     <div id="siteSelect" class="col-sm-10" data-bind="foreach:sites">
      <div  class="col-md-4 col-sm-6"">
       <input type="checkbox"
     data-bind="checkedValue: SiteId, checked: $root.selectedSites, text: Name">
       </input>
      </div>
     </div>
    </div>
    

    In my veiwmodel, I have:

    var vm = {
        sites: ko.observableArray([
            {SiteId: 1, Name: 'Site 1'},
            {SiteId: 2, Name: 'Site 2'},
            {SiteId: 2, Name: 'Site 3'}
        ]),
        selelctedSites: ko.observableArray([1]),
    }
    return vm;
    

    This tells knockout to create a checkbox for each item in the array 'sites', display the Name property of each element, and make the SiteId the value (bind: checkedValue) for each check box. The'selectedSites' array will have an element for each selected item, and that element will be the 'checkedValue', or in this case, the SiteId. As shown above, the checkbox for 'Site 1' will be checked by default. You could also have the checkedValue be the entire object, if that works better (giving you an array of selected objects). This, I think, usually works better than trying to coordinate two arrays.

    There are also options for a select box (single- or multi-select), if that is your preferred interface.

    Hope this helps, let me know if you have more questions.