Sina Saeedi
Sina Saeedi

Reputation: 45

Update ViewModel in Knockout

I'm new to Knockout. I'm trying to develop a Shopping Cart functionality using Knockout.

My problem is when I want to put a Counter in my cart, the counter will be applied to all the cart contents.

var FoodVM = function() {
  self.ID = ko.observable();
  self.Name = ko.observable();
  self.Price = ko.observable();
  self.Clicks = ko.observable(0);

  self.clickCounterAdd = function() {
    self.Clicks(self.Clicks() + 1);
  };
};

var FoodsListVM = function() {
  this.FoodsList = ko.observableArray([new FoodVM()]);
};

$(document).ready(function() {
  var model = new FoodsListVM();
  ko.applyBindings(model);

  var data = [{
      ID: 1,
      Name: "Test 1",
      Price: 25000
    },
    {
      ID: 2,
      Name: "Test 2",
      Price: 30000
    },
    {
      ID: 3,
      Name: "Test 3",
      Price: 35000
    }
  ];

  model.FoodsList(data);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<table border="1">
  <tr>
    <th>ID</th>
    <th>Name</th>
    <th>Price</th>
    <th>Add</th>
    <th>Count</th>
  </tr>
  <tbody data-bind="foreach: FoodsList">
    <tr>
      <td data-bind="text: ID"></td>
      <td data-bind="text: Name"></td>
      <td data-bind="text: Price"></td>
      <td>
        <button data-bind="click: clickCounterAdd" style="width: 73px;">Add</button>
      </td>
      <td>
        <input type="text" data-bind="value: Clicks" style="width: 20px;">
      </td>
    </tr>
</table>

JSFiddle demo.

Could you please tell me where is my mistake?

Upvotes: 1

Views: 1417

Answers (2)

Phani Pulapa
Phani Pulapa

Reputation: 61

look the changes below

var Food = function(params) {
  var self = this;

  self.ID = ko.observable(params.ID);
  self.Name = ko.observable(params.Name);
  self.Price = ko.observable(params.Price);

  self.clicks = ko.observable(0);
  self.countClick = function () {
    self.clicks(self.clicks() + 1);
  };
};

var FoodsList = function(params) {
  var self = this; 

  self.foods = ko.observableArray(params.foods.map(function (item) {
    return new Food(item);
  }));
};


// ----------------------------------------------------------------------
var model = {
  foods: [
    { ID: 1, Name: "Test 1", Price: 25000 },
    { ID: 2, Name: "Test 2", Price: 30000 },
    { ID: 3, Name: "Test 3", Price: 35000 }
  ]
};

var vm = new FoodsList(model);
ko.applyBindings(vm);

Upvotes: 0

Tomalak
Tomalak

Reputation: 338208

Your first mistake is forgetting to define self in your FoodVM.

var self = this;

Because of this you were attaching all the observables and the clickCounterAdd function to whatever self happened to be in the global scope, not to your viewmodel.

The next mistake is that you initialize your FoodsList property with a single FoodVM...

this.FoodsList = ko.observableArray([new FoodVM()]);

...but a few moments later you overwrite that with a simple JS object, data.

model.FoodsList(data);

That means the one FoodVM is lost - and the items in data do not become FoodVMs on their own. Some kind of loop is necessary to turn the plain objects in the data array into FoodVM instances. Usually this is done through a .map() call.


Here is a better approach to viewmodel building.

Write your viewmodels in such a way that they initialize themselves from a parameter object (i.e. from the model). Make the property names of your data match the property names of your viewmodel.

function someViewmodel(params) {
    this.data = ko.observable(params.data);
}

In your case there are two viewmodels, Food and FoodsList (I find it redundant to call them *VM, so I'm not doing that). I've added one level to the model (foods) to match the equally-named property in the FoodsList.

var Food = function(params) {
  var self = this;
  
  self.ID = ko.observable(params.ID);
  self.Name = ko.observable(params.Name);
  self.Price = ko.observable(params.Price);

  self.clicks = ko.observable(0);
  self.countClick = function () {
    self.clicks(self.clicks() + 1);
  };
};

var FoodsList = function(params) {
  var self = this; 

  self.foods = ko.observableArray(params.foods.map(function (item) {
    return new Food(item);
  }));
};


// ----------------------------------------------------------------------
var model = {
  foods: [
    { ID: 1, Name: "Test 1", Price: 25000 },
    { ID: 2, Name: "Test 2", Price: 30000 },
    { ID: 3, Name: "Test 3", Price: 35000 }
  ]
};

var vm = new FoodsList(model);
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<table border="1">
  <tr>
    <th>ID</th>
    <th>Name</th>
    <th>Price</th>
    <th>Add</th>
    <th>Count</th>
  </tr>
  <tbody data-bind="foreach: foods">
    <tr>
      <td data-bind="text: ID"></td>
      <td data-bind="text: Name"></td>
      <td data-bind="text: Price"></td>
      <td>
        <button data-bind="click: countClick" style="width: 73px;">Add</button>
      </td>
      <td>
        <input type="text" data-bind="value: clicks" style="width: 20px;">
      </td>
    </tr>
</table>

Note how everything bootstraps itself during initialization, new FoodsList(model) builds an entire, nested viewmodel.

Incidentally this task is so common that there is a knockout plugin that does it for you: http://knockoutjs.com/documentation/plugins-mapping.html. Take some time to read its documentation.

Upvotes: 1

Related Questions