Knockout: Can't get specific value from observableArray - javascript

I created array with data about some music albums in viewmodel. I can use this array in foreach loop (for example) but I can't display only one value from this array.
My viewmodel:
function CD(data) {
this.Author = ko.observable(data.Author);
this.Title = ko.observable(data.Title);
this.Price = ko.observable(data.Price);
this.Label = ko.observable(data.Label);
this.Id = ko.observable(data.Id);
}
function NewsListViewModel() {
var self = this;
self.newsList = ko.observableArray([]);
$.getJSON("/music/news", function (allData) {
var mapped = $.map(allData, function (item) { return new CD(item) });
self.newsList(mapped);
});
self.oneObject = ko.computed(function () {
return self.newsList()[0].Title;
});
}
$(document).ready(function () {
ko.applyBindings(new NewsListViewModel());
});
And how I used it in html:
<ul data-bind="foreach: newsList">
<li data-bind="text: Title"></li>
<li data-bind="text: Author"></li>
<li data-bind="text: Price"></li>
<li data-bind="text: Label"></li>
</ul>
This works perfectly. But if I trying additionally display one value:
<ul data-bind="foreach: newsList">
<li data-bind="text: Title"></li>
<li data-bind="text: Author"></li>
<li data-bind="text: Price"></li>
<li data-bind="text: Label"></li>
</ul>
...
<p data-bind="text: oneObject"</p>
it dosen't working, firebug thrown error:
Type error: self.newsList()[0].Title is undefined
Why I cannot get one specific value but I can display whole list?
PS. Code <p data-bind="text: newsList()[0].Title"</p> didn't works too.

You get the error because $.getJSON is asynchronous.
So when your computed is declared your items are not necessary ready in the newsList so there is no first element what you could access.
So you need to check in your computed that your newsList is not empty (anyway your server could also return an empty list so it is always good to have this check) and only then access the first element:
self.oneObject = ko.computed(function () {
if (self.newsList().length > 0)
return self.newsList()[0].Title();
return 'no title yet';
});
Note: you need also write .Title() to get its value because Title is an observable.

I can see a few problems with your code:
$.getJSON is asynchronous so self.newsList is empty when it's accessed by Knockout for the first time. Which in turn means that self.newsList[0] is undefined.
return self.newsList()[0].Title; in the computed observable doesn't return the value you want it to return. It should be return self.newsList()[0].Title();
You don't have to reinvent the wheel, there is the mapping plugin which you can use to map the data: http://knockoutjs.com/documentation/plugins-mapping.html

Related

Knockout.js -Getting error - Uncaught ReferenceError: Unable to process binding "with: function"

I am working on a neighborhood map project and I am stuck! I am new to knockout.js. I am trying to use data-bind getting this error -
knockout-3.4.1.js:72 Uncaught ReferenceError: Unable to process binding "with: function (){return filteredItems }"
The snippet of HTML source -
section class="main">
<form class="search" method="post" action="index.html" >
<input type="text" data-bind="textInput: filter" placeholder="Click here/Type the name of the place">
<ul data-bind="with: filteredItems">
<li><span data-bind="text: title, click: $parent.showInfoWindow"></span></li>
</ul>
</form>
</section>
and this is my viewModel -
function viewModel(markers) {
var self = this;
self.filter = ko.observable(''); // this is for the search box, takes value in it and searches for it in the array
self.items = ko.observableArray(locations); // we have made the array of locations into a ko.observableArray
// attributed to - http://www.knockmeout.net/2011/04/utility-functions-in-knockoutjs.html , filtering through array
self.filteredItems = ko.computed(function() {
var filter = self.filter().toLowerCase();
if (!filter) {
return self.items();
} else {
return ko.utils.arrayFilter(self.items(), function(id) {
return stringStartsWith(id.name.toLowerCase(), self.filter);
});
}
});
var stringStartsWith = function (string, startsWith) {
string = string || "";
if (startsWith.length > string.length)
return false;
return string.substring(0, startsWith.length) === startsWith;
};
// populateInfoWindow(self.filteredItems,)
// this.showInfoWindow = function(place) { // this should show the infowindow if any place on the list is clicked
// google.maps.event.trigger(place.marker, 'click');
// };
}
Some lines are commented because I am still working on it. To see the whole project- https://github.com/Krishna-D-Sahoo/frontend-nanodegree-neighborhood-map
The with binding creates a new binding context with the provided element. The error is thrown because of a reference to title within the <span> element, but filteredItems does not have a title property.
If you want to display a <li> element for each element in the filteredItems array, you can use a foreach binding, like this:
<ul data-bind="foreach: filteredItems">
<li><span data-bind="text: title, click: $parent.showInfoWindow"></span></li>
</ul>

Knockout JS failed to update observableArray

So I'm trying to add content to an observable array, but it doesn't update. The problem is not the first level content, but the sub array. It's a small comments section.
Basically I've this function to declare the comments
function comment(id, name, date, comment) {
var self = this;
self.id = id;
self.name = ko.observable(name);
self.date = ko.observable(date);
self.comment = ko.observable(comment);
self.subcomments = ko.observable([]);
}
I've a function to retrieve the object by the id field
function getCommentByID(id) {
var comment = ko.utils.arrayFirst(self.comments(), function (comment) {
return comment.id === id;
});
return comment;
}
This is where I display my comments
<ul style="padding-left: 0px;" data-bind="foreach: comments">
<li style="display: block;">
<span data-bind="text: name"></span>
<br>
<span data-bind="text: date"></span>
<br>
<span data-bind="text: comment"></span>
<div style="margin-left:40px;">
<ul data-bind="foreach: subcomments">
<li style="display: block;">
<span data-bind="text: name"></span>
<br>
<span data-bind="text: date"></span>
<br>
<span data-bind="text: comment"></span>
</li>
</ul>
<textarea class="comment" placeholder="comment..." data-bind="event: {keypress: $parent.onEnterSubComment}, attr: {'data-id': id }"></textarea>
</div>
</li>
</ul>
And onEnterSubComment is the problematic event form
self.onEnterSubComment = function (data, event) {
if (event.keyCode === 13) {
var id = event.target.getAttribute("data-id");
var obj = getCommentByID(parseInt(id));
var newSubComment = new comment(0, self.currentUser, new Date(), event.target.value);
obj.subcomments().push(newSubComment);
event.target.value = "";
}
return true;
};
It's interesting, because when I try the same operation during initialization(outside of any function) it works fine
var subcomment = new comment(self.commentID, "name1", new Date(), "subcomment goes in here");
self.comments.push(new comment(self.commentID, "name2", new Date(), "some comment goes here"));
obj = getCommentByID(self.commentID);
obj.subcomments().push(subcomment);
If anyone can help me with this, cause I'm kind of stuck :(
You need to make two changes:
1st, you have to declare an observable array:
self.subcomments = ko.observableArray([]);
2nd, you have to use the observable array methods, instead of the array methods. I.e. if you do so:
obj.subcomments().push(subcomment);
If subcomments were declared as array, you'd be using the .push method of Array. But, what you must do so that the observable array detect changes is to use the observableArray methods. I.e, do it like this:
obj.subcomments.push(subcomment);
Please, see this part of observableArray documentation: Manipulating an observableArray:
observableArray exposes a familiar set of functions for modifying the contents of the array and notifying listeners.
All of these functions are equivalent to running the native JavaScript array functions on the underlying array, and then notifying listeners about the change

Minimise memory usage of empty observableArrays in knockout

I have a simple webpage with a large list of products (20,000+). When you can click on a product, it will load (via AJAX) a list of colors and display them inline. Html...
<div data-bind="foreach: products">
<span data-bind="click: $root.loadColors($data), text: $name"></span>
<ul data-bind="foreach: colors">
<li data-bind="text:$data" />
</ul
</div>
Shop view model:
function shopViewModel()
{
var self = this;
self.products = ko.observableArray([]);
self.loadColors = function(product)
{
var data = GetColorsByAjax();
product.colors(data);
}
}
Product view Model:
function productModel(data)
{
var self = this;
self.name = data.name;
self.colors = ko.observableArray([]);
}
When I have 20,000+ products, it uses a lot of memory. Each product has a colors array, which is always empty/null, until the user clicks on it, but it still uses a lot of memory.
Ideally, I'd like to remove the colors observableArray and somehow create it dynamically when user clicks on the product. Or separate it into a new viewModel.
I want to eliminate the empty observableArrays to minimise memory, but can't figure out how it do it.
I would use one of Knockout's control-flow bindings (if, with) to only bind the colors:foreach when there is actually a colors property on the productModel().
HTML:
<div data-bind="foreach: products">
<span data-bind="click: $root.loadColors($data), text: $name"></span>
<div data-bind="if: hasColors">
<ul data-bind="foreach: colors">
<li data-bind="text:$data" />
</ul>
</div>
</div>
Product View Model:
function productModel(data)
{
var self = this;
self.name = data.name;
self.hasColors = ko.observable(false);
self.colors = null;
}
Shop View Model
function shopViewModel()
{
var self = this;
self.products = ko.observableArray([]);
self.loadColors = function(product)
{
var data = GetColorsByAjax();
if(product.colors == null) {
product.colors = ko.observableArray(data);
product.hasColors(true);
} else {
product.colors(data);
}
}
}
You don't have to store an empty observable array: you can default to undefined and Knockout will treat it as an empty array in a foreach binding.
Here's a demonstration: http://jsfiddle.net/zm62T/

Durandal 2.0 - Update the value of an observable in the view

I'm new to Durandal, my question has probably a very simple issue.
I load a list into a dropdown and the current value on the link which display the dropdown,
And the value of the link which display the dropdown is not updated correctly when an other value is selected.
But actually, I can't set the value of the observable in the select function.
View Model
var self = this;
self.system = require('durandal/system');
IPsKeys: ko.observableArray([]),
ipKeys: ko.observable(""),
activate: function (context) {
var that = this;
that.IPsKeys([]);
that.ipKeys("");
return $.when(
service.getIPSbyClientId(context.clientId).then(function (json) {
$.each(json, function (Index, Value) {
var ClientLobUWYear = {
NameLob: Value.LineOfBusiness.Name,
NameUWYear: Value.UnderwritingYear
};
that.IPsKeys().push(ClientLobUWYear);
// HERE MY VALUE IS GOOD UPDATING AND THE BINDING WORK
if (Index=== 0) {
that.ipKeys(ClientLobUWYear);
}
});
})
).then(function () {
//do some other datacontext calls for stuff used directly and only in view1
});
},
select: function (item) {
this.ipKeys = {
IdClient: item.IdClient,
IdLob: item.IdLob,
NameLob: item.NameLob,
NameUWYear: item.NameUWYear
};
/** PROBLEMS HERE **/
/** Uncaught TypeError: undefined is not a function **/
this.ipKeys(ClientLobUWYear);
},
View
<a id="select_lob-UWYear" class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="controls_value" data-bind="text: ipKeys().NameLob">ALOB</span>
<span class="controls_value" data-bind="text: ipKeys().NameUWYear">AYEAR</span>
</a>
<ul id="dropdown_year" class="dropdown-menu" data-bind="foreach: IPsKeys().sort(sortByLobYear)">
<li>
<a href="#" data-bind="click: $parent.select">
<span class="controls_value" data-bind="text: NameLob">Cargo</span>
<span class="controls_value" data-bind="text: NameUWYear">2014</span>
</a>
</li>
</ul>
Thanks a lot
The way you update an observable is like this:
var someObservable = ko.observable(""); //setting to "";
someObservable("Something else"); //updating to "Something else"
Not like this (which you are doing above)
var someObservable = ko.observable(""); //setting to "";
someObservable = "Something else";
This is overwriting someObservable with a string of value "Something else" and so is no longer an observable which is why it will not update the ui.
[JS Fiddle showing how to set observables.]

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