Search code examples
javascripthtmlknockout.jsforeachknockout-2.0

KnockoutJS foreach doesn't iterate through collection


For some reason, foreach in Knockout.js doesn't iterate through my observable array.

In my HTML I have this which works perfectly fine with the observable model:

<div class="field-group">
    <label class="popup-label" for="email">Email</label>
    <span class="email" data-bind="text: masterVM.employeeVM.Email"></span>
</div>

But in the same model, this code doesn't work:

<ul data-bind="foreach: { data: masterVM.employeeVM.Tags, as: 'tag' }">
    <li>
        <span class="popup-tag" data-bind="text: tag.tagName"><i class="zmdi zmdi-delete"></i></span>
    </li>
</ul>

There are two models:

Employee

var observableEmployee = function(id, email, tags) {
    var self = this;

    self.Id = ko.observable(id);
    self.Email = ko.observable(email); 

    self.Tags = ko.observableArray(ko.utils.arrayMap(tags, function(item) {
        return new observableTag(item.Id, item.EmployeeId, item.TagId, item.tagName)
    }));

    self.errors = ko.validation.group(this, {
        deep: true
    });

    self.isValid = ko.computed(function() {
        return self.errors().length > 0 ? false : true;
    });
}

and Tag

var observableTag = function(id, employeeId, tagId, tagName) {
    var self = this;

    self.Id = ko.observable(id);
    self.employeeId = ko.observable(employeeId);
    self.tagId = ko.observable(tagId);
    self.TagName = ko.observable(tagName);

    self.errors = ko.validation.group(this, {
        live: true
    });

    self.isValid = ko.computed(function() {
        return self.errors().length > 0 ? false : true;
    });
}

and handler function:

var employeeHandler = function () {
var self = this;

self.getEmployeeDetails = function (header) {
    $.ajax({
        url: masterVM.controller.renderEmployeeDetails,
        dataType: 'json',
        contentType: 'application/json',
        type: 'POST',
        data: JSON.stringify({ id: header.data("employeeid") }),
        success: function (result) {                

            masterVM.employeeVM = new observableEmployee(
                result.model.Id,
                result.model.Email,
                result.model.Tags
            );
            ko.applyBindings(masterVM, $("#employee-planning-selected")[0]);

            //header.parent().addClass('open');
            //header.next().slideDown('normal');

            //hideLoader(header);
            console.log('get employee details');
            $(document).on('click', "div.employee", onNameCardClick);
        },
        error: function (xhr, ajaxOptions, thrownError) {
            alert('Error!');
        }
    });

}}

In my HTML file

    <script>
        masterVM = {
            controller: {
                renderEmployeeDetails: '@(Html.GetActionUrl<EmployeesController>(c => c.RenderEmployeeDetails(0)))'
            },
            employeeHandler: new employeeHandler(),
            employeeVM: new observableEmployee(0, '', '', '', '')
    }
    ko.applyBindings(masterVM);
</script>

Tried something like this, and still nothing

<!--ko foreach: employeeVM.Tags -->
    <span data-bind="text: $data.Tags"></span>
<!-- /ko -->

And no, there are no errors in the console, I have used KnockouJS context debugger which shows me that there are elements in this collection, even when I try to display them as an object it shows me a list of 4 elements.

Knockout version: 2.3.0


Solution

  • 1). If you are binding masterVM object in ko.applyBindings(masterVM), you don't need to specify that object again in your data-bindings.

    So, it should be

    foreach: { data: employeeVM.Tags, as: 'tag' }
    

    And not

    foreach: { data: masterVM.employeeVM.Tags, as: 'tag' }
    

    (I'm not sure how the first data-bind="text: masterVM.employeeVM.Email" is working)


    2). You don't need to call applyBindings more than once. If you want to update the employee object, you can turn your employeeVM into an observable and keep updating it inside getEmployeeDetails method.


    3) Your containerless control flow syntax won't work. (<!--ko foreach: employeeVM.Tags -->). Inside this foreach, $data is the current Tag object in context. So, it should be <span data-bind="text: $data.TagName"></span>


    Here's a minimal version of the code. Click on "Run code snippet" to test it. When you click on Update employee button, I'm updating the employeeVM observable and the data gets rendered again. Without calling applyBindings again

    var employeeHandler = function() {
      var self = this;
    
      self.getEmployeeDetails = function(header) {
        var newEmployee = new observableEmployee(0, '[email protected]', [{
          Id: 3,
          EmployeeId: 3,
          TagId: 3,
          tagName: 'Tag Name 3'
        }]);
      
       // You need to use employeeVM(newEmployee) instead of employeeVM = newEmployee
       // Because employeeVM is an observable.
        masterVM.employeeVM(newEmployee);
      }
    }
    
    var observableEmployee = function(id, email, tags) {
      var self = this;
    
      self.Id = ko.observable(id);
      self.Email = ko.observable(email);
    
      self.Tags = ko.observableArray(ko.utils.arrayMap(tags, function(item) {
        return new observableTag(item.Id, item.EmployeeId, item.TagId, item.tagName)
      }));
    }
    
    var observableTag = function(id, employeeId, tagId, tagName) {
      var self = this;
    
      self.Id = ko.observable(id);
      self.employeeId = ko.observable(employeeId);
      self.tagId = ko.observable(tagId);
      self.TagName = ko.observable(tagName);
    }
    
    var masterVM = {
      controller: {
        renderEmployeeDetails: ''
      },
      employeeHandler: new employeeHandler(),
      // change this to an observable
      employeeVM: ko.observable(new observableEmployee(0, '[email protected]', [{
        Id: 1,
        EmployeeId: 1,
        TagId: 1,
        tagName: 'Tag name 1'
      }]))
    }
    
    ko.applyBindings(masterVM);
    
    document.getElementById("button").addEventListener("click", function(e) {
      masterVM.employeeHandler.getEmployeeDetails()
    })
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    <div class="field-group">
      <label class="popup-label" for="email">Email:</label>
      <span class="email" data-bind="text: employeeVM().Email"></span>
    </div>
    
    <ul data-bind="foreach: { data: employeeVM().Tags, as: 'tag' }">
      <li>
        <span class="popup-tag" data-bind="text: tag.employeeId"></span> <br>
        <span class="popup-tag" data-bind="text: tag.tagId"></span><br>
        <span class="popup-tag" data-bind="text: tag.TagName"></span>
      </li>
    </ul>
    
    <button id="button">Update employee</button>