Øyvind Bråthen
Øyvind Bråthen

Reputation: 60694

Knockout mapping and bindings

I have some problems with nested view models in knockout using the mapping plugin. I'm able to recreate the problem, and I have created a fiddle for it here: Fiddle

I have stripped down the actual view and viewmodel, so don't expect the output to look nice, but it will get the message accros. This is my view:

 <div data-bind="foreach: $root.selectedArmy().Units">
    <div class="unitoverview">
        <!-- ko foreach: UnitMembers-->
        <div class="member">
            <div>
                <span class="name" data-bind="text: Name, click: $parent.RemoveTest"></span>
            </div>
            <div data-bind="foreach: test">
                <span data-bind="text:$data, click: $parent.RemoveTest"></span>
            </div>
            <h1 data-bind="text: test2"></h1>
        </div>
        <!-- /ko -->
    </div>
</div>
<span data-bind="click:AddUnit">CLICK TO ADD UNIT</span>

And this is my model:

var armymaker = armymaker || {};

var unitMapping = {
  'UnitMembers': {
    create: function (options) {
      return new UnitMemberViewModel(options.data);
    }
  }
};

var UnitViewModel = function (unit) {
  var self = this;
  self.Name = ko.observable("unitname");
  self.UnitDefinitionId = ko.observable(unit.Id);
  ko.mapping.fromJS(unit, {}, self);
};

var UnitMemberViewModel = function (unitmemberdefinition) {
  var self = this;

  self.test = ko.observableArray([ko.observable('TEST'), ko.observable('TEST2')]);
  self.test2 = ko.observable('TEST1');
  self.RemoveTest = function () {
    self.test.splice(0,1); 
    self.Name('BUGFACE');
    self.test2('OKI!!');
  };
  ko.mapping.fromJS(unitmemberdefinition, {}, self);
};

var ViewModel = function () {
  var self = this;
  self.showLoader = ko.observable(false);
  self.newArmy = ko.observable({});
  self.unitToAdd = ko.observable(null);
  self.selectedArmy = ko.observable({ Template: ko.observable(''), Units: ko.observableArray() });
  self.AddUnit = function () {
    var data = {'Name': 'My name', 'UnitMembers': [
        { 'Name': 'Unitname1' }
    ] };
    self.unitToAdd(new UnitViewModel((ko.mapping.fromJS(data, unitMapping))));
    self.selectedArmy().Units.push(self.unitToAdd());
    self.unitToAdd(null);
  };
};

armymaker.viewmodel = new ViewModel();
ko.applyBindings(armymaker.viewmodel);

What happens is the following:

I click the link CLICK TO ADD UNIT, and that created a UnitViewModel, and for each element in the UnitMember array it will use the UnitMemberViewModel because of the custom binder (unitMapper) that I am using.

This all seems to work fine. However in the innermost view model, I add some field to the datamodel. I have called them test that is an observableArray, and test2 that is an ordinary observable. I have also created a method called RemoveTest that is bound in the view to both the span that represent test2, and the span in the foreach that represent each element of the array test.

However when I invoke the method, the change to the observable is reflected in the view, but no changes to the observableArray is visible in the view. Check the fiddle for details.

Are there any reasons why changes to an obsArray will not be visible in the view, but changes to an ordinary observable will?

I have made some observations:

There is probably a flaw with my model, but I can't see it so I would appreciate some help here.

Upvotes: 1

Views: 612

Answers (1)

nemesv
nemesv

Reputation: 139748

You are basically "double mapping".

First with

self.unitToAdd(new UnitViewModel((ko.mapping.fromJS(data, unitMapping))));

and the second time inside the UnitViewModel:

ko.mapping.fromJS(unit, {}, self);

where the unit is already an ko.mapping created complete "UnitViewModel", this double mapping leads to all of your problems.

To fix it you just need to remove the first mapping:

self.unitToAdd(new UnitViewModel(data));
self.selectedArmy().Units.push(self.unitToAdd());
self.unitToAdd(null);

and use the mapping option inside the UnitViewModel:

var UnitViewModel = function (unit) {
    var self = this;
    self.Name = ko.observable("unitname");
    self.UnitDefinitionId = ko.observable(unit.Id);
    ko.mapping.fromJS(unit, unitMapping, self);
};

Demo JSFiddle.

SideNote to fix the "The click event on the observable does not work" problem you just need to remove the $parent:

<span class="name" data-bind="text: Name, click: RemoveTest"></span>

because you are already in the context of one UnitMemberViewModel.

Upvotes: 1

Related Questions