Knockout does not update UI when observableArray is a property of other model

I have 2 observableArray connected to each other. When a "feature" is clicked, I try to show "tasks" of it. However KO, does not update UI when I clicked a feature. On the console, I can track my viewModel, and I can see tasks successfully loaded on selectedFeature. However, UI does not update, even all arrays are defined as observable.

Here is a live demo on fiddle.

Please tell me where I am missing?

function GetFeatures() {
    var url = "/Project/GetFeatures";

    $.get(url, "", function (data) {
        $.each(JSON.parse(data), function (i, item) {
            projectVM.features.push(new featureViewModelCreator(item, projectVM.selectedFeature));
        });
    });

};

function GetTasks(selectedFeature) {
    var url = "/Task/GetTaskList";
    $.get(url, { "FeatureId": selectedFeature.FeatureId }, function (data) {

        $.each(JSON.parse(data), function (i, item) {
            selectedFeature.tasks.push(new taskViewModelCreator(item, selectedFeature.selectedTask));
        });
    });
};

function taskViewModelCreator(data, selected) {
    var self = this;
    self.TaskId = data.TaskId;
    self.Title = data.Name;
    self.Status = data.Status.Name;
    self.CreatedDate = data.CreatedDate;
    self.UserCreatedFullName = data.UserCreated.FullName;

    this.IsSelected = ko.computed(function () {
        return selected() === self;
    });
}

function featureViewModelCreator(data, selected) {
    var self = this;
    self.FeatureId = data.FeatureId;
    self.Name = data.Name;
    self.Status = data.Status.Name;
    self.CreatedDate = data.CreatedDate;
    self.UserCreatedFullName = data.UserCreated.FullName;

    self.tasks = ko.observableArray();

    this.IsSelected = ko.computed(function () {
        return selected() === self;
    });

    self.selectedTask = ko.observable();
    self.taskClicked = function (clickedTask) {
        var selection = ko.utils.arrayFilter(self.model.tasks(), function (item) {
            return clickedTask === item;
        })[0];
        self.selectedTask(selection);
    }
}

function projectViewModelCreator() {
    var self = this;
    self.ProjectId = 1;
    self.features = ko.observableArray();
    self.selectedFeature = ko.observable();

    self.featureClicked = function (clickedFeature) {
        self.selectedFeature(clickedFeature);
        GetTasks(clickedFeature);
    }
}
var projectVM = new projectViewModelCreator();


ko.applyBindings(projectVM, $('.taskmanTable')[0]);

GetFeatures();

On the UI

<div class="taskmanTable">
    <table class="table table-hover featureList">
        <thead>
            <tr>
                <th>Title</th>
            </tr>
        </thead>
        <tbody data-bind="foreach: features">
            <tr data-bind="click: $root.featureClicked, css: { active : IsSelected } ">
                <td><span data-bind="text: Name"> </span></td>
            </tr>
        </tbody>
    </table>
    <table class="table table-hover taskList">
        <thead>
            <tr>
                <th>Title</th>
            </tr>
        </thead>
        <tbody data-bind="foreach: selectedFeature.tasks">
            <tr>
                <td><span data-bind="text:Title"></span></td>
            </tr>
        </tbody>
    </table>
</div>

Answers


Here is the correct version with key notes: here. KO documentation is quite a detailed one.


You have mentioned an interesting note about UI code style: "As I know, we don't use () on UI". I did not put attention to this fact before.

  1. We can really omit brackets for an observable: ko observable;

View contains an observable with no brackets:

<label>
<input type="checkbox" data-bind="checked: displayMessage" /> Display message
</label>

Source code:

ko.applyBindings({
    displayMessage: ko.observable(false)
});
  1. We can omit brackets for an observable array on UI: ko observable array

View contains: <ul data-bind="foreach: people">, while View model has:

self.people = ko.observableArray([
        { name: 'Bert' },
        { name: 'Charles' },
        { name: 'Denise' }
    ]);
  1. We can omit brackets on UI for 'leaf' observables or observables arrays. Here is your modified code sample. data-bind="if: selectedFeature" and data-bind="foreach: selectedFeature().tasks"> only leaf observable braces are omitted.

  2. Finally, can we omit brackets for 'parent' observables? We can do it by adding another ko UI-statement (with instead of if, example 2).

The with binding will dynamically add or remove descendant elements depending on whether the associated value is null/undefined or not

  1. But, I believe, we can not omit brackets for parent nodes outside UI statement, because it is equal to a javascript statement: projectVM.selectedfeature().tasks. Othervise projectVM.selectedfeature.tasks will not work, because observables does not have such property tasks. Instead an observable contains an object with that property, which is retireved by calling it via brackets (). There is, actually, an example on knockoutjs introduction page. <button data-bind="enable: myItems().length < 5">Add</button>

The code below uses the following fact (which can be found here, example 2):

It’s important to understand that the if binding really is vital to make this code work properly. Without it, there would be an error when trying to evaluate capital.cityName in the context of “Mercury” where capital is null. In JavaScript, you’re not allowed to evaluate subproperties of null or undefined values.

function GetFeatures() {
  var data = {
    Name: "Test Feature",
    FeatureId: 1
  }
  projectVM.features.push(new featureViewModelCreator(data, projectVM.selectedFeature));

};

function GetTasks(selectedFeature) {
  var data = {
      Title: "Test Feature",
      TaskId: 1
  }
  selectedFeature().tasks.push(new taskViewModelCreator(data, selectedFeature().selectedTask));
};

function taskViewModelCreator(data, selected) {
  var self = this;
  self.TaskId = data.TaskId;
  self.Title = data.Title;
  // Step 3: you can omit $root declaration, I have removed it
  // just to show that the example will work without $root as well.
  // But you can define the root prefix explicitly (declaring explicit
  // scope may help you when you models become more complicated).
  
  // Step 4: data-bind="if: selectedFeature() statement was added
  // to hide the table when it is not defined, this statement also
  // helps us to avoid 'undefined' error.
  
  // Step 5: if the object is defined, we should referense 
  // the observable array via -> () as well. This is the KnockoutJS
  // style we have to make several bugs of that kind in order
  // to use such syntax automatically.

  this.IsSelected = ko.computed(function() {
    return selected() === self;
  });
}

function featureViewModelCreator(data, selected) {
  var self = this;
  self.FeatureId = data.FeatureId;
  self.Name = data.Name;


  self.tasks = ko.observableArray();

  this.IsSelected = ko.computed(function() {
    return selected() === self;
  });

  self.selectedTask = ko.observable();
  
}

function projectViewModelCreator() {
  var self = this;
  self.ProjectId = 1;
  self.features = ko.observableArray();
  self.selectedFeature = ko.observable();

  self.featureClicked = function(clickedFeature) {
    self.selectedFeature(clickedFeature);
    GetTasks(self.selectedFeature);
  }
}

var projectVM = new projectViewModelCreator();

ko.applyBindings(projectVM, $('.taskmanTable')[0]);

GetFeatures();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="taskmanTable">
  <table class="table table-hover featureList">
    <thead>
      <tr>
        <th>Title</th>
      </tr>
    </thead>
    <tbody data-bind="foreach: features">
      <tr data-bind="click: $root.featureClicked, css: { active : IsSelected } ">
        <td><span data-bind="text: Name"> </span></td>
      </tr>
    </tbody>
  </table>
  <hr/>
  <table data-bind="if: selectedFeature()" class="table table-hover taskList">
    <thead>
      <tr>
        <th>Title</th>
      </tr>
    </thead>
    <tbody data-bind="foreach: selectedFeature().tasks()"><!-- $root -->
      <tr>
        <td><span data-bind="text: Title"></span></td>
      </tr>
    </tbody>
  </table>
</div>

Need Your Help

jQuery form selector doesnt seem to work

javascript jquery

I am trying to put a generic form submit function.

Java printing symbol for positive number

java

I have a Java program that does a bunch of calculations from some user inputs and at the end it has to print the equation of the plane. Format of the equation of the plane is 6x-2y+3z-4=0.