1

I'm creating a list of checkboxes using a knockout.js foreach loop. I have an observable array that holds the value of the checkboxes, but when I call the function to check their values the this element is referring to the parent element i.e. the viewModel. How can I access the element of the array that relates to the element that is clicked on?

I have tried removing the $parent reference from the following code, but that threw this error: Message: getSampleValue is not defined

This is the foreach loop that creates the checkboxes:

<!-- ko foreach: sampleList -->
            <div data-bind="css: { hasValue: $parent.getSampleValue() }">
                <span data-bind="click: $parent.sampleClick, text: name"></span>
                <div data-bind="click: $parent.sampleClick" class="spot fa fa-square"></div>
            </div>
<!-- /ko -->

And these are the functions defined in the ko view model:

sampleViewModel = function (data) {
   var self = this;
   self.sampleList = ko.observableArray(data.sampleList);

    self.sampleClick = function () {
       this.isChecked = !this.isChecked;
    }

    self.getSampleValue = function () {
       return this.isChecked;
    }
}

And the sampleList is in the format:

{
  {
    "name": "name1",
    "isChecked": false
  },
  {
    "name": "name2",
    "isChecked": true
  }
}

Expected results: The function getSampleValue should be called when the value of isChecked for a given item in the array is changed and that should update the value on the UI. The isChecked value that is returned from that function should be of the correct element in the array and not the parent element.

Actual results: The function is called, but appears to be only called on creation of the checkboxes and not when the isChecked value is updated, and the this within the function is the parent element of the caller and therefore doesn't have an isChecked value.

Any help with this is much appreciated.

EDIT: So changing this line:

<div data-bind="css: { hasValue: $parent.getSampleValue() }">

To this:

<div data-bind="css: { hasValue: isChecked }">

Seems to display the correct values on loading, but they still don't update on the UI even though the values of the observable array are being changed.

2 Answers 2

2

The function in your view model has this bound to the view model itself. You can access the current element with the first argument of your handler function:

sampleViewModel = function (data) {
    var self = this;
    self.sampleList = ko.observableArray(data.sampleList);

    self.sampleClick = function (element) {
       element.isChecked = !element.isChecked;
    }

    self.getSampleValue = function (element) {
       return element.isChecked;
    }
}

See The "click" binding:

When calling your handler, Knockout will supply the current model value as the first parameter. This is particularly useful if you’re rendering some UI for each item in a collection, and you need to know which item’s UI was clicked.


Also, to correctly bind to getSampleValue you should remove the parentheses.

<div data-bind="css: { hasValue: $parent.getSampleValue }">

or - as you already found out - since the function returns the value without modifying it, you can bind to the value itself:

<div data-bind="css: { hasValue: isChecked }">
Sign up to request clarification or add additional context in comments.

Comments

0

You are using Knockout the wrong way around. Don't put methods that belong to an item into the item's parent. Let the items be responsible for their own state.

Consider the example below:

  • The SampleItem class keeps track of its own name and its own "checked"-status. It also offers a method to toggle the "checked" status.
  • The SampleApplication class only keeps a list of items. It will serve as the $root viewmodel of the page.
  • The view uses isChecked to decide on the CSS class for Font Awesome, and transparently links the click event to the right receiver.
  • The entire viewmodel graph is built from a single function call, constructing nested viewmodels and mapping data to observables in the process.
  • No cross-referencing from child to $parent is necessary.

function SampleItem(data) {
  var self = this;
  
  self.name = ko.observable(data.name);
  self.isChecked = ko.observable(data.isChecked);
  
  self.toggleChecked = function () {
    self.isChecked(!self.isChecked());
  };
}

function SampleApplication(data) {
  var self = this;
  
  self.sampleList = ko.observableArray();
  
  // viewmodel init (i.e. data mapping)
  data.sampleList.forEach(function (itemData) {
    self.sampleList.push( new SampleItem(itemData) );
  });
}

// -------------------------------------------------------
var rawModelData = {
  sampleList: [{
    name: "name1",
    isChecked: false
  }, {
    name: "name2",
    isChecked: true
  }]
};

var vm = new SampleApplication(rawModelData);
ko.applyBindings(vm);
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<!-- ko foreach: sampleList -->
<div data-bind="click: toggleChecked">
  <span data-bind="text: name"></span>
  <div class="spot fa-square" data-bind="css: {far: !isChecked(), fas: isChecked()}"></div>
</div>
<!-- /ko -->

<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)">


Using the mapping plugin the process of creating nested viewmodels and keeping them up to date with server data becomes easier, especially when there is a lot of data to map to observables and viewmodel nesting structure gets more complex.

A simple call to ko.mapping.fromJS(inputObj, options, target) takes care of all the details.

function SampleItem(data) {
  var self = this;

  self.name = ko.observable("");
  self.isChecked = ko.observable(false);

  // viewmodel init (i.e. data mapping)
  ko.mapping.fromJS(data, {}, self);
  
  self.toggleChecked = function () {
    self.isChecked(!self.isChecked());
  };
}

function SampleApplication(data) {
  var self = this;
  
  self.sampleList = ko.observableArray();
  
  // viewmodel init (i.e. data mapping)
  ko.mapping.fromJS(data, SampleApplication.mapping, self);
}
SampleApplication.mapping = {
  sampleList: {
    create: options => new SampleItem(options.data)
  }
};
// -------------------------------------------------------
var rawModelData = {
  sampleList: [{
    name: "name1",
    isChecked: false
  }, {
    name: "name2",
    isChecked: true
  }]
};

var vm = new SampleApplication(rawModelData);
ko.applyBindings(vm);
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>

<!-- ko foreach: sampleList -->
<div data-bind="click: toggleChecked">
  <span data-bind="text: name"></span>
  <div class="spot fa-square" data-bind="css: {far: !isChecked(), fas: isChecked()}"></div>
</div>
<!-- /ko -->

<hr>
<pre data-bind="text: ko.toJSON(ko.mapping.toJS($root), null, 2)">

Note that in this variant, "unmapping" (via ko.mapping.toJS()) is necessary to remove all the properties the mapping plugin adds to keep track of things.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.