Knockout.js - Data binding outputting function text when not using parens - javascript

I am new to Knockout and have been trying to follow code examples and the documentation, but keep running into an issue. My data bindings printing the Knockout observable function, not the actual values held by my observable fields. I can get the value if I evaluate the field using (), but if you do this you do not get any live data-binding / updates.
Below are some code snippets from my project that are directly related to the issue I am describing:
HTML
<div class="col-xs-6">
<div data-bind="foreach: leftColSocialAPIs">
<div class="social-metric">
<img data-bind="attr: { src: iconPath }" />
<strong data-bind="text: name"></strong>:
<span data-bind="text: totalCount"></span>
</div>
</div>
</div>
Note: leftColSocialAPIs contains an array of SocialAPIs. I can show that code too if needed.
Initializing the totalcount attribute
var SocialAPI = (function (_super) {
__extends(SocialAPI, _super);
function SocialAPI(json) {
_super.call(this, json);
this.totalCount = ko.observable(0);
this.templateName = "social-template";
}
SocialAPI.prototype.querySuccess = function () {
this.isLoaded(true);
appManager.increaseBadgeCount(this.totalCount());
ga('send', 'event', 'API Load', 'API Load - ' + this.name, appManager.getRedactedURL());
};
SocialAPI.prototype.toJSON = function () {
var self = this;
return {
name: self.name,
isActive: self.isActive(),
type: "social"
};
};
return SocialAPI;
})(API);
Updating totalcount attribute for LinkedIn
var LinkedIn = (function (_super) {
__extends(LinkedIn, _super);
function LinkedIn(json) {
json.name = "LinkedIn";
json.iconPath = "/images/icons/linkedin-16x16.png";
_super.call(this, json);
}
LinkedIn.prototype.queryData = function () {
this.isLoaded(false);
this.totalCount(0);
$.get("http://www.linkedin.com/countserv/count/share", { "url": appManager.getURL(), "format": "json" }, this.queryCallback.bind(this), "json").fail(this.queryFail.bind(this));
};
LinkedIn.prototype.queryCallback = function (results) {
if (results != undefined) {
results.count = parseInt(results.count);
this.totalCount(isNaN(results.count) ? 0 : results.count);
}
this.querySuccess();
};
return LinkedIn;
})(SocialAPI);
In the <span data-bind="text: totalCount"></span>, I expect to see a number ranging from 0-Integer.MAX. Instead I see the following:
As you can see, its outputting the knockout function itself, not the value of the function. Every code example I've seen, including those in the official documentation, says that I should be seeing the value, not the function. What am I doing wrong here? I can provide the full application code if needed.

Not sure, but KO view models obviously tend to bind own (not inherited through prototypes) observable properties only. So you should rewrite your code to supply totalCount observable for every social network separately.

Related

Mapping a nested object as an observable from a complex JSON using the create callback

I've got a complex object in a JSON format. I'm using Knockout Mapping, customizing the create callback, and trying to make sure that every object that should be an observable - would actually be mapped as such.
The following code is an example of what I've got:
It enables the user to add cartItems, save them (as a JSON), empty the cart, and then load the saved items.
The loading part fails: It doesn't display the loaded option (i.e., the loaded cartItemName). I guess it's related to some mismatch between the objects in the options list and the object bounded as the cartItemName (see this post), but I can't figure it out.
Code (fiddle):
var cartItemsAsJson = "";
var handlerVM = function () {
var self = this;
self.cartItems = ko.observableArray([]);
self.availableProducts = ko.observableArray([]);
self.language = ko.observable();
self.init = function () {
self.initProducts();
self.language("english");
}
self.initProducts = function () {
self.availableProducts.push(
new productVM("Shelf", ['White', 'Brown']),
new productVM("Door", ['Green', 'Blue', 'Pink']),
new productVM("Window", ['Red', 'Orange'])
);
}
self.getProducts = function () {
return self.availableProducts;
}
self.getProductName = function (product) {
if (product) {
return self.language() == "english" ?
product.productName().english : product.productName().french;
}
}
self.getProductValue = function (selectedProduct) {
// if not caption
if (selectedProduct) {
var matched = ko.utils.arrayFirst(self.availableProducts(), function (product) {
return product.productName().english == selectedProduct.productName().english;
});
return matched;
}
}
self.getProductColours = function (selectedProduct) {
selectedProduct = selectedProduct();
if (selectedProduct) {
return selectedProduct.availableColours();
}
}
self.addCartItem = function () {
self.cartItems.push(new cartItemVM());
}
self.emptyCart = function () {
self.cartItems([]);
}
self.saveCart = function () {
cartItemsAsJson = ko.toJSON(self.cartItems);
console.log(cartItemsAsJson);
}
self.loadCart = function () {
var loadedCartItems = ko.mapping.fromJSON(cartItemsAsJson, {
create: function(options) {
return new cartItemVM(options.data);
}
});
self.cartItems(loadedCartItems());
}
}
var productVM = function (name, availableColours, data) {
var self = this;
self.productName = ko.observable({ english: name, french: name + "eux" });
self.availableColours = ko.observableArray(availableColours);
}
var cartItemVM = function (data) {
var self = this;
self.cartItemName = data ?
ko.observable(ko.mapping.fromJS(data.cartItemName)) :
ko.observable();
self.cartItemColour = data ?
ko.observable(data.cartItemColour) :
ko.observable();
}
var handler = new handlerVM();
handler.init();
ko.applyBindings(handler);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://rawgit.com/SteveSanderson/knockout.mapping/master/build/output/knockout.mapping-latest.js
"></script>
<div>
<div data-bind="foreach: cartItems">
<div>
<select data-bind="options: $parent.getProducts(),
optionsText: function (item) { return $parent.getProductName(item); },
optionsValue: function (item) { return $parent.getProductValue(item); },
optionsCaption: 'Choose a product',
value: cartItemName"
>
</select>
</div>
<div>
<select data-bind="options: $parent.getProductColours(cartItemName),
optionsText: $data,
optionsCaption: 'Choose a colour',
value: cartItemColour,
visible: cartItemName() != undefined"
>
</select>
</div>
</div>
<div>
<button data-bind="text: 'add cart item', click: addCartItem" />
<button data-bind="text: 'empty cart', click: emptyCart" />
<button data-bind="text: 'save cart', click: saveCart" />
<button data-bind="text: 'load cart', click: loadCart" />
</div>
</div>
What needs to be changed to fix it?
P.S.: I've got another piece of code (see it here) that demonstrates a persistance of the selected value even after changing the options - though there optionsValue is a simple string, while here it's an object.
EDIT:
I figured out the problem: the call ko.mapping.fromJS(data.cartItemName) creates a new productVM object, which is not one of the objects inside availableProducts array. As a result, none of the options corresponds to the productVM contained in the loaded cartItemName, so Knockout thereby clears the value altogether and passes undefined.
But the question remains: how can this be fixed?
In the transition from ViewModel -> plain object -> ViewModel you loose the relation between the products in your cart and the ones in your handlerVM.
A common solution is to, when loading a plain object, manually search for the existing viewmodels and reference those instead. I.e.:
We create a new cartItemVM from the plain object
Inside its cartItemName, there's an object that does not exist in handlerVM.
We look in handlerVM for a product that resembles this object, and replace the object by the one we find.
In code, inside loadCart, before setting the new viewmodels:
loadedCartItems().forEach(
ci => {
// Find out which product we have:
const newProduct = ci.cartItemName().productName;
const linkedProduct = self.availableProducts()
.find(p => p.productName().english === newProduct.english());
// Replace the newProduct by the one that is in `handlerVM`
ci.cartItemName(linkedProduct)
}
)
Fiddle: https://jsfiddle.net/7z6010jz/
As you can see, the equality comparison is kind of ugly. We look for the english product name and use it to determine the match. You can also see there's a difference in what is observable and what isn't.
My advice would be to use unique id properties for your product, and start using those. You can create a simpler optionsValue binding and matching new and old values happens automatically. If you like, I can show you an example of this refactor as well. Let me know if that'd help.

Databinding with Knockout JS not working

I'm trying to bind my JSON object to a HTML div but none of the bindings seem to work. This is my current try on the subject. But I have tried using the template binding already. That resulted in an undefined error , but the object is correctly loaded because i always get it in the console.
$(document).ready(function () {
var submissionViewModel = new SubmissionModel();
submissionViewModel.getSubmission().done(function () {
ko.applyBindings(submissionViewModel, document.getElementById("submission"));
})
});
var SubmissionModel = function () {
var self = this;
//self.loading = ko.observableArray();
self.Submission = ko.observable(null);
self.getSubmission = function () {
// Let loading indicator know that there's a new loading task that ought to complete
//self.loading.push(true);
return $.getJSON('/Submission/GetSubmission',
function (data) {
console.log("submission loading")
console.dir(data);
self.Submission = ko.mapping.fromJSON(JSON.stringify(data));
}
);
}
}
HTML
<div id="submission" data-bind="with: Submission">
<span data-bind="text: SubmissionTitle"></span>
</div>
JSON
"{"
SubmissionID":"1be87a85-6d95-43aa-ad3c-ffa047b759a5",
"SubmissionTitle":"nog wat sliden",
"SubmissionDescription":"////",
"SubmissionFile":"TESTFILE ",
"CreatedOn":"2015-09-02T21:10:54.913",
"SubmissionPoolID":"5af408f5-515c-4994-88dd-dbb2e4a242a2",
"SubmissionTypeID":1,
"CreatedBy":"a028a47d-3104-4ea4-8fa6-7abbb2d69bbd
"}"
I have been chewing on this problem for a few days now an I can't seem to get it to work. Could any of you point me in the right direction ?
In java-script to decode object inside string you need to use JSON.parse and make sure your object is not structured in such way double quote inside double quote .
viewModel:
var json = '{"SubmissionID":"1be87a85-6d95-43aa-ad3c-ffa047b759a5","SubmissionTitle":"nogwatsliden","SubmissionDescription":"--","SubmissionFile":"TESTFILE ","CreatedOn":"2015-09-02T21:10:54.913","SubmissionPoolID":"5af408f5-515c-4994-88dd-dbb2e4a242a2","SubmissionTypeID":1,"CreatedBy":"a028a47d-3104-4ea48fa67abbb2d69bbd"}'
var ViewModel = function () {
this.Submission = ko.observable();
this.Submission(ko.mapping.fromJS(JSON.parse(json)));
//you can also use ko.mapping.fromJSON(json) as jeroen pointed out
};
ko.applyBindings(new ViewModel());
working sample here

Knockout Nested Bindings--Visible in DOM but won't display

I've got an issue where my viewmodel has an observable object that contains observable properties. When I try to access those properties they don't display. I can, however, see that all the properties with values are visible in the DOM using the Knockout chrome extension.
My code looks like:
viewmodel:
self.device=ko.observable();
self.device(querydevice.query({"url": self.url, "ref":self.ref}));
query code:
define(['jquery','knockout','hsd'], function ($,ko, device) {
return{
query:function (params) {
var hsdevice=ko.observable();
self.url=params.url;
self.ref=params.ref;
var controlData = $.getJSON(self.url + "/JSON?request=getcontrol&ref=" + self.ref);
var statusData = $.getJSON(self.url + "/JSON?request=getstatus&ref=" + self.ref);
$.when(controlData, statusData).done(function (_cdata, _sdata) {
var data = $.extend(_cdata[0], _sdata[0]);
hsdevice(new device(data));
});
return hsdevice;
}};
});
device object:
define(['knockout'], function (ko) {
return function device (data){
var self=this;
self.deviceName = ko.observable(data.Devices[0].name);
self.value = ko.observable(data.Devices[0].value);
self.status =ko.observable(data.Devices[0].status);
self.controlPairs = ko.observableArray();
ko.utils.arrayPushAll(self.controlPairs, data.ControlPairs);
};
});
This is what I see being returned:
" device": Object
controlPairs: Array[2]
deviceName: "Garage Hall Light"
status: "Off"
value: 0
In my HTML I have this:
<span class="tile-title align-" data-bind="with: device.deviceName"></span>
I've also tried using data-bind:"text: device().deviceName", but that doesn't work either. Nothing displays. I can however access over observable properties that are on the viewmodel. The only difference is that they're single level properties with no sub-binding. So I am able to see something like self.test("test") in my html but not my self.device with the nested databinds.
Any ideas what I'm doing wrong?
Thanks!
It looks like you are using jquery promises. what you need to do is return the $.when
something like
define(['jquery','knockout','hsd'], function ($,ko, device) {
return{
query:function (params) {
self.url=params.url;
self.ref=params.ref;
var controlData = $.getJSON(self.url + "/JSON?request=getcontrol&ref=" + self.ref);
var statusData = $.getJSON(self.url + "/JSON?request=getstatus&ref=" + self.ref);
return $.when(controlData, statusData).done(function (_cdata, _sdata) {
var data = $.extend(_cdata[0], _sdata[0]);
return new device(data);
});
}};
});
then you end up with something like this.
querydevice.query({"url": self.url, "ref":self.ref})
.when(function(data){
self.device(data);
return true;
});
Thanks to Nathan for his code contribution. I was finally able to access my nested properties in the html by using:
<!-- ko with: device -->
<!-- /ko -->
and THEN data-bind to the property I needed.

Knockout computed observable not firing 'write'

I have a fairly simple array of objects that can be edited in KO
Here's a test case. Try clicking on the items and editing them down below. It works.
However...
The data loaded into the array comes from a JSON string:
self.text = ko.observable('[{ "value": "1", "text": "Low" }, ..... ]');
This must be parsed and converted into a JS object. This is done in the computed function like this:
self.ssArray = ko.computed({
read: function() {
// Convert text into JS object
// Not using ko.utils because I want to use try/catch to detect bad JS later
var arrayJS = JSON.parse(ko.utils.unwrapObservable(self.text));
// Make an array of observables
// Not using ko.mapping in order to get back to basics
// Also mapping function throws an error re: iterations or something
var obsArrayJS = ko.utils.arrayMap(arrayJS, function(i) {
return {
"value": ko.observable(i.value),
"text": ko.observable(i.text)
};
});
// return array of objects with observable properties.
return obsArrayJS;
// Tried this but made no difference:
//return ko.observableArray(obsArrayJS);
},
Now what I want is for the original text string to be updated whenever the model is updated. It should be a simple case of ko.toJSON on the model:
write: function(value) {
self.text(ko.toJSON(this.ssArray));
},
As you can see from the fiddle, self.text is not updated.
Why is this?
I have tried the following:
returning an observableArray from the read function - makes no difference
return an observableArray of observable objects each with observable properties
using the mapping plugin to make everything possible observable
I guess it boils down to how KO knows to fire the write function. Surely if the contents of ssArray change then write is fired? But not in my case...
Possible further complication is that this will be a KO component. The text input will actually come from a parameter passed from the widget. So I guess it will already be an observable? So it will need to update the parent viewmodel too.
In addition to this I'm trying to use the sortable plugin to allow reordering of these items - but I've removed that from my test case.
The 'write' function of your computed is not firing, because you are not writing to the computed — that would mean calling ssArray(some_value) somewhere.
This is an alternative solution that works:
We create an observableArray named items for our individual text/value pairs
This observableArray is populated by calling loadJSON manually.
We create a computed that establishes subscriptions to the items observableArray, as well as to all the items text and value observables by iterating over them. Whenever either items are added or removed or change, we serialize the whole array back to JSON
You could certainly subscribe to self.text and trigger loadJSON automatically, but then you will have to take care of the circle of 'text' triggering 'loadJSON', triggering our computed, writing back to text.
(I have hidden the code snippets in order to get rid of the HTML and CSS code blocks. Click "Show code snippet" to run the examples.)
function MyViewModel() {
var self = this;
this.selectedItemSS = ko.observable();
this.setSelectedSS = function(item) {
self.selectedItemSS(item);
};
// Data in text form. Passed in here as a parameter from parent component
this.text = ko.observable('[{"value": "1", "text": "Low"}, {"value": "2", "text": "Medium"}, {"value": "3", "text": "High"} ]');
this.items = ko.observableArray([]);
this.loadJSON = function loadJSON(json) {
var arrayOfObjects = JSON.parse(json),
arrayOfObservables;
// clear out everything, or otherwise we'll end
// up with duplicated objects when we update
self.items.removeAll();
arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) {
return {
text: ko.observable(object.text),
value: ko.observable(object.value)
};
});
self.items(arrayOfObservables);
};
this.loadJSON( this.text() );
ko.computed(function() {
var items = this.items();
// iterate over all observables in order
// for our computed to get a subscription to them
ko.utils.arrayForEach(items, function(item) {
item.text();
item.value();
});
this.text(ko.toJSON(items));
}, this);
}
ko.applyBindings(new MyViewModel());
function MyViewModel() {
var self = this;
this.selectedItemSS = ko.observable();
this.setSelectedSS = function(item) {
self.selectedItemSS(item);
};
// Data in text form. Passed in here as a parameter from parent component
this.text = ko.observable('[ \
{\
"value": "1",\
"text": "Low"\
},\
{ \
"value": "2",\
"text": "Medium"\
},\
{\
"value": "3",\
"text": "High"\
} ]');
this.items = ko.observableArray([]);
this.loadJSON = function loadJSON(json) {
var arrayOfObjects = JSON.parse(json),
arrayOfObservables;
// clear out everything, or otherwise we'll end
// up with duplicated objects when we update
self.items.removeAll();
arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) {
return {
text: ko.observable(object.text),
value: ko.observable(object.value)
};
});
self.items(arrayOfObservables);
};
this.loadJSON( this.text() );
ko.computed(function() {
var items = this.items();
// iterate over all observables in order
// for our computed to get a subscription to them
ko.utils.arrayForEach(items, function(item) {
item.text();
item.value();
});
this.text(ko.toJSON(items));
}, this);
}
ko.applyBindings(new MyViewModel());
body { font-family: arial; font-size: 14px; }
.well {background-color:#eee; padding:10px;}
pre {white-space:pre-wrap;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<h3>Text Json: eg from AJAX request</h3>
<p>In practice this comes from a parent custom component as a parameter</p>
<pre class="well" data-bind="text:text"></pre>
<h3>Computed data model</h3>
<p>Click on an item to edit that record</p>
<div data-bind="foreach:items" class="well">
<div data-bind="click: $parent.setSelectedSS">
<span data-bind="text:value"></span>
<span data-bind="text:text"></span><br/>
</div>
</div>
<hr/>
<h3>Editor</h3>
<div data-bind="with:selectedItemSS" class="well">
<input data-bind="textInput:value"/>
<span data-bind="text:value"></span><br/>
</div>
If you prefer, here is an alternative version that handles both changes to the JSON as well as edits through the interface through a single computed:
function MyViewModel(externalObservable) {
var self = this;
this.selectedItemSS = ko.observable();
this.setSelectedSS = function(item) {
self.selectedItemSS(item);
};
// just for the demo
this.messages = ko.observableArray([]);
this.items = ko.observableArray([]);
this.json = externalObservable;
this.previous_json = '';
ko.computed(function() {
var items = this.items(),
json = this.json();
// If the JSON hasn't changed compared to the previous run,
// that means we were called because an item was edited
if (json === this.previous_json) {
var new_json = ko.toJSON(items);
self.messages.unshift("items were edited, updating JSON: " + new_json);
this.previous_json = new_json;
this.json(new_json);
return;
}
// If we end up here, that means that the JSON has changed compared
// to the last run
self.messages.unshift("JSON has changed, updating items: " + json);
var arrayOfObjects = JSON.parse(json),
arrayOfObservables;
// clear out everything, or otherwise we'll end
// up with duplicated objects when we update
this.items.removeAll();
arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) {
return {
text: ko.observable(object.text),
value: ko.observable(object.value)
};
});
// iterate over all observables in order
// for our computed to get a subscription to them
ko.utils.arrayForEach(arrayOfObservables, function(item) {
item.text();
item.value();
});
this.items(arrayOfObservables);
this.previous_json = json;
}, this);
}
var externalObservableFromParam = ko.observable(),
viewModel;
// Pretend here that this observable was handed to us
// from your components' params
externalObservableFromParam('[{"value": "1", "text": "Low"}, {"value": "2", "text": "Medium"}, {"value": "3", "text": "High"} ]');
viewModel = new MyViewModel(externalObservableFromParam);
ko.applyBindings(viewModel);
function MyViewModel(externalObservable) {
var self = this;
this.selectedItemSS = ko.observable();
this.setSelectedSS = function(item) {
self.selectedItemSS(item);
};
// just for the demo
this.messages = ko.observableArray([]);
this.items = ko.observableArray([]);
this.json = externalObservable;
this.previous_json = '';
ko.computed(function() {
var items = this.items(),
json = this.json();
// If the JSON hasn't changed compared to the previous run,
// that means we were called because an item was edited
if (json === this.previous_json) {
var new_json = ko.toJSON(items);
self.messages.unshift("items were edited, updating JSON: " + new_json);
this.previous_json = new_json;
this.json(new_json);
return;
}
// If we end up here, that means that the JSON has changed compared
// to the last run
self.messages.unshift("JSON has changed, updating items: " + json);
var arrayOfObjects = JSON.parse(json),
arrayOfObservables;
// clear out everything, or otherwise we'll end
// up with duplicated objects when we update
this.items.removeAll();
arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) {
return {
text: ko.observable(object.text),
value: ko.observable(object.value)
};
});
// iterate over all observables in order
// for our computed to get a subscription to them
ko.utils.arrayForEach(arrayOfObservables, function(item) {
item.text();
item.value();
});
this.items(arrayOfObservables);
this.previous_json = json;
}, this);
}
var externalObservableFromParam = ko.observable(),
viewModel;
// Pretend here that this observable was handed to us
// from your components' params
externalObservableFromParam('[{"value": "1", "text": "Low"}, {"value": "2", "text": "Medium"}, {"value": "3", "text": "High"} ]');
viewModel = new MyViewModel(externalObservableFromParam);
ko.applyBindings(viewModel);
body {
font-family: arial;
font-size: 14px;
}
.well {
background-color: #eee;
padding: 10px;
}
pre {
white-space: pre-wrap;
}
ul {
list-style-position: inside;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<h3>Text Json: eg from AJAX request</h3>
<p>In practice this comes from a parent custom component as a parameter</p>
<pre class="well" data-bind="text: json"></pre>
<textarea data-bind="value: json" cols=50 rows=5></textarea>
<h3>Computed data model</h3>
<p>Click on an item to edit that record</p>
<div data-bind="foreach: items" class="well">
<div data-bind="click: $parent.setSelectedSS">
<span data-bind="text:value"></span>
<span data-bind="text:text"></span>
<br/>
</div>
</div>
<hr/>
<h3>Editor</h3>
<div data-bind="with:selectedItemSS" class="well">
<input data-bind="textInput:value" />
<span data-bind="text:value"></span>
<br/>
</div>
<hr/>
<h3>Console</h3>
<ul data-bind="foreach: messages" class="well">
<li data-bind="text: $data"></li>
</ul>

Filter users by one keyword in a nested observableArray

I am trying to filter my users observableArray which has a nested keywords observableArray
based on a keywords observableArray on my viewModel.
When I try to use ko.utils.arrayForEach I get a stack overflow exception. See the code below, also posted in this jsfiddle
function User(id, name, keywords){
return {
id: ko.observable(id),
name: ko.observable(name),
keywords: ko.observableArray(keywords),
isVisible: ko.dependentObservable(function(){
var visible = false;
if (viewModel.selectedKeyword() || viewModel.keywordIsDirty()) {
ko.utils.arrayForEach(keywords, function(keyword) {
if (keyword === viewModel.selectedKeyword()){
visible = true;
}
});
if (!visible) {
viewModel.users.remove(this);
}
}
return visible;
})
}
};
function Keyword(count, word){
return{
count: ko.observable(count),
word: ko.observable(word)
}
};
var viewModel = {
users: ko.observableArray([]),
keywords: ko.observableArray([]),
selectedKeyword: ko.observable(),
keywordIsDirty: ko.observable(false)
}
viewModel.selectedKeyword.subscribe(function () {
if (!viewModel.keywordIsDirty()) {
viewModel.keywordIsDirty(true);
}
});
ko.applyBindings(viewModel);
for (var i = 0; i < 500; i++) {
viewModel.users.push(
new User(i, "Man " + i, ["Beer", "Women", "Food"])
)
}
viewModel.keywords.push(new Keyword(1, "Beer"));
viewModel.keywords.push(new Keyword(2, "Women"));
viewModel.keywords.push(new Keyword(3, "Food"));
viewModel.keywords.push(new Keyword(4, "Cooking"));
And the View code:
<ul data-bind="template: { name: 'keyword-template', foreach: keywords }"></ul><br />
<ul data-bind="template: { name: 'user-template', foreach: users }"></ul>
<script id="keyword-template" type="text/html">
<li>
<label><input type="radio" value="${word}" name="keywordgroup" data-bind="checked: viewModel.selectedKeyword" /> ${ word }<label>
</li>
</script>
<script id="user-template" type="text/html">
<li>
<span data-bind="visible: isVisible">${ $data.name }</span>
</li>
</script>
Your isVisible dependentObservable has created a dependency on itself and is recursively trying to evaluate itself based on this line:
if (!visible) {
viewModel.users.remove(this);
}
So, this creates a dependency on viewModel.users, because remove has to access the observableArray's underlying array to remove the user. At the point that the array is modified, subscribers are notified and one of the subscribers will be itself.
It is generally best to not change the state of any observables in a dependentObservable. you can manually subscribe to changes to a dependentObservable and makes your changes there (provided the dependentObservable does not depend on what you are changing).
However, in this case, I would probably instead create a dependentObservable at the viewModel level called something like filteredUsers. Then, return a version of the users array that is filtered.
It might look like this:
viewModel.filteredUsers = ko.dependentObservable(function() {
var selected = viewModel.selectedKeyword();
//if nothing is selected, then return an empty array
return !selected ? [] : ko.utils.arrayFilter(this.users(), function(user) {
//otherwise, filter on keywords. Stop on first match.
return ko.utils.arrayFirst(user.keywords(), function(keyword) {
return keyword === selected;
}) != null; //doesn't have to be a boolean, but just trying to be clear in sample
});
}, viewModel);
You also should not need the dirty flag, as dependentObservables will be re-triggered when any observables that they access have changed. So, since it accesses selectedKeyword, it will get re-evaluated whenever selectedKeyword changes.
http://jsfiddle.net/rniemeyer/mD8SK/
I hope that I properly understood your scenario.

Categories