Domanda

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:

  • The click event on the observable does not work, only the click event on the elements on the observableArray.
  • It seems that self inside the click event does not match the actual viewmodel. If I go self.test.splice(0,1) nothing happens in the view, but self.test.splice only contains one element after that command. However if I traverse the base viewmodel (armymaker.viewmodel.Units()[0].UnitMembers()[0].test) is still contains two elements.
  • Calling splice on the traversed viewmodel (armymaker.viewmodel.Units()[0].UnitMembers()[0].test.splice(0,1)) removes the element from the view, so it seems in some way that the element referenced by self is not the same object as what is linked inside the view. But then, why does it work for the observable that is not an array?

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

È stato utile?

Soluzione

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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top