Reputation: 83
I'm writing a Knockout.js application which performs basic CRUD operations on a graph of entities. I load two JSON-encoded objects from my server: the first is an array containing a bunch of product details, the second is a list of indexed images as follows:
function UploadedImage(data) {
this.UploadedImageId = data.UploadedImageId;
this.Url = data.Url;
this.Name = data.Name;
this.AltText = data.AltText;
}
...which is stored in the ViewModel via an AJAX request as follows:
$.post("@Url.Action("JsonRepairData")", {}, function (response) {
self.repairData(_.map(response.repairData, function(d) { return new AppleProduct(d); }));
self.images(_.map(response.images, function (i) { return new UploadedImage(i); }));
self.dataLoaded(true);
}, 'json');
I've verified that the data is all loaded correctly, both in the object graph and in the image array. The image IDs in the object graph correspond to the image IDs in the array. To display what image is selected for relevant data entries in the object graph, I opted for a select list as follows:
<tbody data-bind="foreach: repairData">
<tr>
<td>@Html.TextBoxFor(m => m.Name, new { data_bind = "value: Name" })</td>
<td><select data-bind="options: $root.images, optionsCaption: 'Pick an image...', optionsValue: 'UploadedImageId', optionsText: 'Name', value: UploadedImageId"></select></td>
<td data-bind="html: ImgTag"></td>
<td><a href="#" data-bind="click: $root.deleteProduct">delete</a></td>
</tr>
</tbody>
Note that repairData, which is contained in the ViewModel, is the aforementioned object graph. All other data binds correctly. The select box successfully populates. The value of UploadedImageId as bound in the ViewModel updates as the select box is manipulated. However, the ViewModel's initial value of UploadedImageId (from the object graph) is disregarded and when the page renders, UploadImageId is immediately updated to undefined. This is how the UploadedImageId's initial value is set in the object graph:
function AppleProduct(data) {
var productSelf = this;
this.Id = data.Id;
this.Name = ko.observable(data.Name);
this.UploadedImageId = ko.observable(data.UploadedImageId);
this.Variations = ko.observableArray(_.map(data.Variations, function (v) { return new ProductSeries(v); }));
this.ImgTag = ko.computed(function () {
var img = _.find(self.images(), function (i) { return i.UploadedImageId == productSelf.UploadedImageId(); });
return img ? '<img src="'+img.Url+'" />' : '';
});
// Remove computed ImgTag from data addressed to the server
this.toJSON = function () {
var copy = ko.toJS(this);
delete copy.ImgTag;
return copy;
};
}
I've been trying to figure out how to get the select box to set correctly for hours and I'm still completely stumped. All I can determine is that in the 2.1.0 knockout js source file the code responsible for setting the initial value of the select box on line 1402 (see below) is passed twice. The first time the model values properly correspond to the image IDs, but element.options.length == 0 and so the model value isn't used. The second time this code is passed, the model values are all undefined but the select box is populated (and so element.options.length is a positive number), and so again the proper initial value is not set.
for (var i = element.options.length - 1; i >= 0; i--) {
if (ko.selectExtensions.readValue(element.options[i]) == value) {
element.selectedIndex = i;
break;
}
}
Any help in understanding what I'm missing would be greatly appreciated!
Upvotes: 3
Views: 273
Reputation: 83
As nemesv has cleverly spotted, the problem was the ordering of the population of the ViewModel data:
$.post("@Url.Action("JsonRepairData")", {}, function (response) {
self.repairData(_.map(response.repairData, function(d) { return new AppleProduct(d); }));
self.images(_.map(response.images, function (i) { return new UploadedImage(i); }));
self.dataLoaded(true);
}, 'json');
Due to the aggressiveness of Knockout.js dependency tracking, the system attempts to populate the object graph views before it even has access to image data. That's why when it tries to create select boxes, the select boxes have no data to populate them and their default values can't be set. By switching the order in which repairData and images ViewModel data load, image data becomes available when it's needed and the application runs.
This seems to demonstrate that Knockout.js dependency tracking via ko.observables, whereas a powerful expressive tool, can be dangerous if proper care is not taken with unintended logic resulting from ViewModel state mutation.
Upvotes: 3