Search code examples
javascriptjqueryknockout.jsknockout-mapping-plugin

Knockout, collection property becomes null when I try and remove one item from it if it has more than one item in it?


Really strange knockout error I am having. It's a pretty complex scenario so please see this fiddle:

http://jsfiddle.net/yx8dkLnc/

Essentially I have a double nested collection, the first collection FishMeasurements contains a collection of objects which have the species information associated with it, and a collection of Measurements which hold all measurements associated with that species.

Now when I try and remove items from the nested collection in this HTML:

<!-- ko foreach: FishMeasurements() -->
                <h3><span data-bind="text: SpeciesName"></span><span data-bind="text: SpeciesId" style="display: none;"></span></h3>
                <table class="table table-striped">
                    <thead>
                        <tr>
                            <th>Length</th>
                            <th>Count</th>
                            <th>Weight</th>
                            <th>Fish Id</th>
                            <th>&nbsp;</th>
                        </tr>
                    </thead>
                    <tbody data-bind="foreach: Measurements()">
                        <tr>
                            <td><span data-bind="text: LengthInMillimeters"></span></td>
                            <td><span data-bind="text: Count"></span></td>
                            <td><span data-bind="text: WeightInPounds"></span></td>
                            <td><span data-bind="text: FishCode"></span></td>
                            <td><a href="#" data-bind="click: function() { $root.removeMeasurement($data, $index); }">Remove</a></td>
                        </tr>
                    </tbody>
                </table>
            <!-- /ko -->

The remove measurement function doesn't work when the Measurements collection has more than one object. I click the remove link, and it throws an error that says:

VM617:163 Uncaught TypeError: Cannot read property 'Measurements' of null(…)

The strange thing about this is, if I only add one item to the Measurements collection, the delete button work fine, but as soon as I add multiple measurements, if I click remove on any item in the table but the first row, this error is generated. However, if I click the first item in the table per species, there is no error and all records are removed!

Something tells me its treating Measurements like one object instead of a collection, because it only works on index 0. But I'm not sure because in console I am able to type:

mappedModel.FishMeasurements()[0].Measurements()[1]

And get a full ko_mapping object returned, so it's not null. But for some reason when I click the remove, it is null. As long as there is only one measurement per species, clicking remove works fine, as soon as there are more it breaks.

What am I doing wrong?


Solution

  • When you addMeasurement for the first time speciesId, speciesName are getting defined because fishMeasurementBySpecies === undefined and therefore when you remove the first item you have a valid measurement.SpeciesId() as a parameter inside removeMeasurement function but for the second time and more since fishMeasurementBySpecies is not undefined anymore then speciesId, speciesName never get set and then whenremoveMeasurementis called,measurement.SpeciesId() is null.

    In order to make your model works, you need to apply below changes.

    define var speciesId = mappedModel.SelectedSpecies(); before your if statment

    var speciesId = mappedModel.SelectedSpecies();
    if (fishMeasurementBySpecies === undefined || fishMeasurementBySpecies === null) {
    

    Put () for Measurements inside removeMeasurement function where you want to get the length

    if(fishMeasurementBySpecies.Measurements().length === 0) 
    

    Below I provide you an example of what you want to do by using manual view model instead of using mapping plugin.


    Example: https://jsfiddle.net/kyr6w2x3/118/

    Your example :http://jsfiddle.net/yx8dkLnc/1/

    VM:

    var data = {
        "AvailableSpecies":
                [
                {"Id":"f57830b8-0766-4374-b481-82c04087415e","Name":"Alabama Shad"},    
              {"Id":"3787ce10-e61c-4f03-88a5-ff648bb55480","Name":"Alewife"},{"Id":"e923214f-4974-4663-9158-d6979ce637f1","Name":"All Sunfish Spp Ex Bass And Crappie"}          ],
            "SelectedSpecies": null,  "CountToAdd":0,"LengthToAdd":0,"WeightToAdd":0,"GenerateFishCode":false,"FishMeasurements":[]
    };
    function AppViewModel(){
       var self = this;
       self.AvailableSpecies = ko.observableArray(data.AvailableSpecies);
       self.SelectedSpecies = ko.observable();
       self.CountToAdd = ko.observable();
       self.LengthToAdd = ko.observable();
       self.WeightToAdd = ko.observable();
       self.FishCode = ko.observable();
       self.FishMeasurements = ko.observableArray([]);
    
       self.addMeasurement = function(item) {
         var SpeciesExists = false;
    
         ko.utils.arrayForEach(self.FishMeasurements(), function (item) {
           if(item.SpeciesId() == self.SelectedSpecies().Id) {
             var len = item.Measurements().length;
             // you may have a better way to generate a unique Id if an item is removed 
             while(item.Measurements().findIndex(x => x.Id() === len) > 0){
                len++;
             }
             item.Measurements.push(new MeasurementsViewModel({LengthInMillimeters:self.LengthToAdd(),
                                                               Count:self.CountToAdd(),
                                                               WeightInPounds:self.WeightToAdd(),
                                                               FishCode:self.FishCode(),
                                                               Id:len ++,
                                                               ParentId:self.SelectedSpecies().Id
                                                              })
                                   );
             SpeciesExists = true;
           }
         });
         if(!SpeciesExists){
           self.FishMeasurements.push(new FishMeasurementsViewModel({SpeciesName:self.SelectedSpecies().Name,
                                                                     SpeciesId:self.SelectedSpecies().Id,
                                                                     Measurements:[{LengthInMillimeters:self.LengthToAdd(),
                                                                                    Count:self.CountToAdd(),
                                                                                    WeightInPounds:self.WeightToAdd(),
                                                                                    FishCode:self.FishCode(),
                                                                                    Id:1}]
                                                                    })
                                     );
         }
       }
       self.removeMeasurement = function(data){
           ko.utils.arrayForEach(self.FishMeasurements(), function (item) {
                if(item && item.SpeciesId() == data.ParentId()) {
                ko.utils.arrayForEach(item.Measurements(), function (subItem) {
                  if(subItem && subItem.Id() == data.Id()) {
                                    item.Measurements.remove(subItem);
                  }
                });
              }
              if(item && item.Measurements().length == 0){
                 self.FishMeasurements.remove(item);
              }
           });
       }
     }
      var FishMeasurementsViewModel = function(data){
       var self = this;
       self.SpeciesName = ko.observable(data.SpeciesName);
       self.SpeciesId = ko.observable(data.SpeciesId);
       self.Measurements = ko.observableArray($.map(data.Measurements, function (item) {
         return new MeasurementsViewModel(item,self.SpeciesId());
       }));
    
     }
     var MeasurementsViewModel = function(data,parentId){
       var self = this;
       self.LengthInMillimeters = ko.observable(data.LengthInMillimeters);
       self.Count = ko.observable(data.Count);
       self.WeightInPounds = ko.observable(data.WeightInPounds);
       self.FishCode = ko.observable(data.FishCode);
       self.Id = ko.observable(data.Id);
       self.ParentId = ko.observable(parentId ? parentId : data.ParentId);
     }
    
    var viewModel = new AppViewModel();
    ko.applyBindings(viewModel);