ObservableArray not reflecting data update - javascript

I'm creating an app using the very slick KnockoutJS library, but I've run into a snag. On the html page, I have a plain <select> control that I want to load with JSON data returned from a web service.
I define the observable array as follows:
var laborRow = function () {
this.positions = ko.observableArray([]);
};
When the page loads, the ajax call is made and the data is returned. In the callback, I do the following:
success: function (msg) {
laborRow.positions = msg;
}
based on the KO docs, I would expect that I would set the result like this:
laborRow.positions(msg);
However, that just throws an error stating that "laborRow.positions in not a function"
The template in the html is as follows:
<tbody data-bind='template: {name: "laborRowTemplate", foreach: laborLine}'> </tbody>
</div>
<script type="text/html" id="laborRowTemplate">
<tr>
<td><select data-bind='options: positions, optionsText: "Title", optionsCaption: "select", value: selectedPosition '></select></td>
</tr>
</script>
The laborRow object is a property on the ViewModel which is bound to the page. For whatever reason, this does not work. To add another wrinkle, if I add code to peek into the observableArray and print out some piece of data, the data is in there. So it is being loaded successfully.
Any thoughts would be greatly appreciated.
The full code for my example case:
var laborRow = function () {
this.positions = ko.observableArray([]);
};
var projectEstimate = function () {
this.laborLine = ko.observableArray([new laborRow()]);
};
var projectViewModel = new projectEstimate();
ko.applyBindings(projectViewModel);
//and the code in the callback function on ajax success
success: function (msg) {
laborRow.positions = msg;
//laborRow.positions(msg); **this does not work - error is laborRow.positions is not a function**
},
And the html:
<tbody data-bind='template: {name: "laborRowTemplate", foreach:
laborLine}'> </tbody>
<script type="text/html" id="laborRowTemplate">
<tr>
<td><select data-bind='options: positions, optionsText:
"Title", optionsCaption: "select", value: selectedPosition '></
select></td>
</tr>
</script>
Finally, thanks to Sean's comments below, I was able to get it working by modifying the code in the callback as follows:
success: function (msg) {
projectViewModel.laborLine()[(projectViewModel.laborLine().length-1)].positionList(msg);
}

The problem is that you haven't actually created your model:
var laborRow = function () {
this.positions = ko.observableArray([]);
// will only be called if you call var some_var = new laborRow()
};
Change your function to a bare object (as shown in the Knockout docs):
var laborRow = {
positions: ko.observableArray([])
};
And you'll be able to call laborRow.positions(msg); and have it work.
EDIT
Based on the new code, laborRow is still not instantiated -- if you are setting var laborRow somewhere else in your code (around the ajax request, perhaps) then you'll want to make sure that your call stack looks like this:
projectViewModel.laborLine()[0].positions()
// This will return the array you're looking for.
// The key is that laborLine is a `getter` not an attribute
I've been bitten by the "ko variables are getters not attributes" bug on several occasions ... might that be happening with your code?

Related

Knockout.js: Get options text from select populated with ajax call

I'm working with a web app using knockout.js as front-end data manipulator, and fetching de data from the server with AJAX calls.
I have a series of drop-downs which are populated via AJAX, using id as values and strings as optionText, and I need to catch, in different divs, the optionText of the selected option of these drop-downs. So far, using different methods from other Stack Overflow answers, I've not been able to do it.
Here is the code:
View
<div>
<select data-bind="options: provinces,optionsText:'name',optionsValue:'id',value: provincesSelected "></select>
<select data-bind="options: regions,optionsText:'name',optionsValue:'id',value: regionsSelected"></select>
<select data-bind="options: cities,optionsText:'name',optionsValue:'id',value:citiesSelected"></select>
</div>
<div>
<div>
<div>
<div>
<span data-bind="text: citiesSelected().name"></span>
</div>
</div>
</div>
</div>
Viewmodel
function getProvinces(provinces) {
$.ajax({
url: "resturl1",
type: "POST",
success: function (data) {
var dats = JSON.parse(data);
provinces(dats);
return provinces;
}
});}function getRegionsByProvince(regions, province) {
$.ajax({
url: "resturl2",
type: "POST",
data: {province: province},
success: function (data) {
var dats = JSON.parse(data);
regions(dats);
return regions;
}
});}function getCitiesByRegion(cities, region) {
$.ajax({
url: "resturl3",
type: "POST",
data: {region: region},
success: function (data) {
var dats = JSON.parse(data);
cities(dats);
return cities;
}
});}
function AppViewModel() {
var self = this;
self.provinces = ko.observableArray();
self.provincesSelected = ko.observable();
self.regions = ko.observableArray([]);
self.regionsSelected = ko.observable();
self.cities = ko.observableArray([]);
self.citiesSelected = ko.observable();
self.provinces(getProvinces(self.provinces));
self.provincesSelected.subscribe(function (val) {
self.regions(getRegionsByProvince(self.regions, val));
});
self.regionsSelected.subscribe(function (val) {
self.cities(getCitiesByRegion(self.cities, val));
});
}ko.applyBindings(new AppViewModel());
This is the last thing I tried (get a "name" property that comes in the array of objects from the server response), but the console throws the error:
Uncaught TypeError: Unable to process binding "text: function (){return citiesSelected().name }"
Message: Cannot read property 'name' of undefined
Not using the property in the div data-bind shows the value, but I need the text.
I'm quite new at Knockout, so maybe I'm making a huge newbie mistake, but I would apreciate a lot your help.
If you remove the below, it works:
optionsValue:'id'
When you set the optionsValue attribute, you are telling Knockout to set selectedCities equal to the id of the selected city. That is why you got the error stating that the name property doesn't exist. You were essentially trying to bind to selectedCities.id.name which definitely doesn't exist. By removing the optionsValue attribute, you will be capturing the entire selected city object, and the binding will evaluate the name property as expected.
Working fiddle: https://jsfiddle.net/dw1284/dqzb3zwn/1/

Having issues tying together basic javascript chat page

I have the skeleton of a chat page but am having issues tying it all together. What I'm trying to do is have messages sent to the server whenever the user clicks send, and also, for the messages shown to update every 3 seconds. Any insights, tips, or general comments would be much appreciated.
Issues right now:
When I fetch, I append the <ul class="messages"></ul> but don't want to reappend messages I've already fetched.
Make sure my chatSend is working correctly but if I run chatSend, then chatFetch, I don't retrieve the message I sent.
var input1 = document.getElementById('input1'), sendbutton = document.getElementById('sendbutton');
function IsEmpty(){
if (input1.value){
sendbutton.removeAttribute('disabled');
} else {
sendbutton.setAttribute('disabled', '');
}
}
input1.onkeyup = IsEmpty;
function chatFetch(){
$.ajax({
url: "https://api.parse.com/1/classes/chats",
dataType: "json",
method: "GET",
success: function(data){
$(".messages").clear();
for(var key in data) {
for(var i in data[key]){
console.log(data[key][i])
$(".messages").append("<li>"+data[key][i].text+"</li>");
}
}
}
})
}
function chatSend(){
$.ajax({
type: "POST",
url: "https://api.parse.com/1/classes/chats",
data: JSON.stringify({text: $('input1.draft').val()}),
success:function(message){
}
})
}
chatFetch();
$("#sendbutton").on('click',chatSend());
This seems like a pretty good project for Knockout.js, especially if you want to make sure you're not re-appending messages you've already sent. Since the library was meant in no small part for that sort of thing, I think it would make sense to leverage it to its full potential. So let's say that your API already takes care of limiting how many messages have come back, searching for the right messages, etc., and focus strictly on the UI. We can start with our Javascript view model of a chat message...
function IM(msg) {
var self = this;
self.username = ko.observable();
self.message = ko.observable();
self.timestamp = ko.observable();
}
This is taking a few liberties and assuming that you get back an IM object which has the name of the user sending the message, and the content, as well as a timestamp for the message. Probably not too far fetched to hope you have access to these data elements, right? Moving on to the large view model encapsulating your IMs...
function vm() {
var self = this;
self.messages = ko.observableArray([]);
self.message = ko.observable(new IM());
self.setup = function () {
self.chatFetch();
self.message().username([user current username] || '');
};
self.chatFetch = function () {
$.getJSON("https://api.parse.com/1/classes/chats", function(results){
for(var key in data) {
// parse your incoming data to get whatever elements you
// can matching the IM view model here then assign it as
// per these examples as closely as possible
var im = new IM();
im.username(data[key][i].username || '');
im.message(data[key][i].message || '');
im.timestamp(data[key][i].message || '');
// the ([JSON data] || '') defaults the property to an
// empty strings so it fails gracefully when no data is
// available to assign to it
self.messages.push(im);
}
});
};
}
All right, so we have out Javascript models which will update the screen via bindings (more on that in a bit) and we're getting and populating data. But how do we update and send IMs? Well, remember that self.message object? We get to use it now.
function vm() {
// ... our setup and initial get code
self.chatSend = function () {
var data = {
'user': self.message().username(),
'text': self.message().message(),
'time': new Date()
};
$.post("https://api.parse.com/1/classes/chats", data, function(result) {
// do whatever you want with the results, if anything
});
// now we update our current messages and load new ones
self.chatFetch();
};
}
All right, so how do we keep track of all of this? Through the magic of bindings. Well, it's not magic, it's pretty intense Javascript inside Knockout.js that listens for changes and the updates the elements accordingly, but you don't have to worry about that. You can just worry about your HTML which should look like this...
<div id="chat">
<ul data-bind="foreach: messages">
<li>
<span data-bind="text: username"></span> :
<span data-bind="text: message"></span> [
<span data-bind="text: timestamp"></span> ]
</li>
</ul>
</div>
<div id="chatInput">
<input data-bind="value: message" type="text" placeholder="message..." />
<button data-bind="click: $root.chatSend()">Send</button>
<div>
Now for the final step to populate your bindings and keep them updated, is to call your view model and its methods...
$(document).ready(function () {
var imVM = new vm();
// perform your initial search and setup
imVM.setup();
// apply the bindings and hook it all together
ko.applyBindings(imVM.messages, $('#chat')[0]);
ko.applyBindings(imVM.message, $('#chatInput')[0]);
// and now update the form every three seconds
setInterval(function() { imVM.chatFetch(); }, 3000);
});
So this should give you a pretty decent start on a chat system in an HTML page. I'll leave the validation, styling, and prettifying as an exercise to the programmer...

Odd behavior when trying to get the contents of a script element

I'm playing around with backbone and underscore templates. When I try to dereference the contents of a script block (using $("#people-templ").html()) I get two different behaviors depending on the context of the call. Inside the render function for the backbone view I get nothing returned. If I get the contents of the script block outside any function, I get the valid HTML contents. I tried this on Chrome, Safari, and Firefox. I stepped through with the debugger and validated that JQuery returned an empty array when calling $("#people-templ") within the callback function. I was wondering if someone had an explanation for this behavior.
In index.html I have a simple template:
<script type="text/template" id="people-templ">
<h1>People</h1>
<table>
<thead>
<tr><th>First Name</th><th>Last Name</th></tr>
</thead>
<tbody>
<% people.each(function(person) { %>
<tr>
<td><%= person.get("firstName") %></td>
<td><%= person.get("lastName") %></td>
</tr>
<% }) %>
</tbody>
</table>
</script>
<script src='/javascripts/lib/jquery-2.0.3.js'></script>
<script src='/javascripts/lib/underscore.js'></script>
<script src='/javascripts/lib/backbone.js'></script>
<script src="/javascripts/main/index.js"></script>
Inside index.js I have the following Backbone view definition:
var peopleHtml = $("#people-templ").html();
var PeopleView = Backbone.View.extend({
el: "#foo",
initialize: function() {
this.render();
},
render: function() {
var people = new People();
var me = this;
this.$el.empty();
people.fetch({
success: function(people) {
var template = _.template(peopleHtml, { people: people });
me.$el.html(template);
},
error: function() {
console.error("Failed to load the collection");
}
});
}
});
This works. The code gets the template from the script tag, Underscore processes it, and inserts the resulting markup into the DOM. If I move var peopleHtml = $("#people-templ").html(); inside of the callback, as such:
var PeopleView = Backbone.View.extend({
el: "#foo",
initialize: function() {
this.render();
},
render: function() {
var people = new People();
var me = this;
this.$el.empty();
people.fetch({
success: function(people) {
var template = _.template($("#people-templ").html(), { people: people });
me.$el.html(template);
},
error: function() {
console.error("Failed to load the collection");
}
});
}
});
Then it seems that nothing is returned from $("people-tempo").html() and the code failed inside of underscore.js when it tries to do text replacement when processing the template.
Does anyone know why this might be the case?
SOLVED thanks to Pointy
<div id="foo"/>
and
<div id="foo"></div>
are not the same thing. If the former is used, everything from the div on down (including all my script elements) were replaced by the templates text. using the latter, only the contents of the div are replaced.
When using elements for underscore templating, it looks like you need to use the
<div id='foo'></div>
form instead of just:
<div id='foo'/>

Multiple Select List and KnockoutJS

I have a multi-select list that I've implemented following the instructions on the KO site. The important portions of my code currently look like this (removed unnecessary code):
function Attribute(data) {
var self = this;
self.Id = data.Id;
self.Name = data.Name;
}
// Individual Row in Table
function Criteria(data) {
var self = this;
self.Attributes = data.Attributes;
}
// Represent the ViewModel for attributes.
function CriteriaViewModel() {
var self = this;
// Catalog Data
self.availableAttributes = window.ko.observableArray([]);
$.getJSON(window.attributeListUrl, function(availableData) {
self.availableAttributes($.map(availableData.Attributes, function(item) { return new Attribute(item); }));
});
// Editable Data
self.criterion = window.ko.observableArray([]);
// Load initial state from server
$.getJSON(window.criteriaListUrl, function (availableData) {
self.criterion($.map(availableData.Criterion, function (item) { return new Criteria(item); }));
});
}
Then, in my HTML, I bind it all together (or, I at least try to):
<tbody data-bind="foreach: criterion">
<tr>
<td>
<select class="selectedAttributes"
data-bind="options: $root.availableAttributes, selectedOptions: Attributes, optionsText: 'Name', optionsValue: 'Id'"
multiple
size="6">
</select>
</td>
</tr>
</tbody>
The possible options display correctly. However, there is no apparent binding between the criteria's attributes against the possible options. From reading the guide, it seems as though KO should be able to bind objects directly. Can anybody provide guidance here?
I forgot to mention that everything works except the actual binding of the multi-select list. I am applying my bindings appropriately in general - just not with the multi-select list.
The attributes property on the Criteria object needs to be an observableArray. Here is a Jsfiddle demonstrating
function Criteria(data) {
var self = this;
self.Attributes = ko.observableArray(data.Attributes);
}
var x= $('#select1 option:selected');
if(x.length>0){
x.each(function(){
alert($(this).text());
self.selectedCategory.push(new categoryModel($(this).text()));
$('#select1 option:selected').remove();
});
}
refer http://jsfiddle.net/deepakpandey1234/wse4gdLq/

Knockout.js: Getting computed observable to return array for use with select options

Basically, what I am trying to achieve is to populate a dropdown based on the value of another dropdown in Knockout.js
My view code(stripped obviously):
<div data-bind="with: chosenMailData">
<table id="tabl-1234" class="mails">
<thead><tr><th>Destination</th><th>Hotel</th></tr></thead>
<tbody data-bind="template: { name: 'iti-template', foreach: objects, afterRender: $root.myPostProcessingLogic }">
</table>
</div>
<script type="text/html" id="iti-template">
<tr>
<td><select class="desti" data-bind="options: $root.destinations, value: destination.name"></select></td>
<td><select data-bind="options: $root.generall(), value: $root.generall()"></select></td>
</tr>
</script>
My View-Model(again stripped-down):
self.destinations=['Kerela', 'Shoghi, Himachal Pradesh', 'Kasauli, Himachal Pradesh'];
self.myPostProcessingLogic = function(elements) {
this.generall = ko.computed(function() {
$('.desti').each(function(i, obj) {
stayOptions = [];
$.getJSON("http://127.0.0.1:8000/api/hotel?format=json&location__name="+obj.value, function(data) {
$.each(data.objects, function(i, item){
stayOptions.push(item.name);
});
});
});
return stayOptions;
}, this);
}
Inserting alert() in this.generall() shows that stayOptions does get populated with the values I want in it. It is getting that array to get displayed in select options for corresponding row in Hotels column which is the problem.
Probably I am making a really dumb mistake but I have been looking at the code for a long time now and nothing comes to mind. Please advise.
EDIT: I am doing this too at the beginning of my view-model:
self.generall = ko.observableArray();
When calling methods, the this variable gets assigned on invocation.
var f = $root.myPostProcessingLogic;
f(); // this will not set 'this' to '$root'
The above is essentially what knockout is doing, and this makes this be bound to something else inside myPostProcessingLogic(). You have already defined the scoped self variable, so this is easy to fix.
Another problem is that, reassigning observables won't preserve any subscribers, and any dependant observables won't update.
self.generall = ko.observableArray();
self.myPostProcessingLogic = function(elements) {
self.generall.removeAll();
$('.desti').each(function(i, obj) {
$.getJSON("http://127.0.0.1:8000/api/hotel?format=json&location__name="+obj.value, function(data) {
$.each(data.objects, function(i, item){
self.generall.push(item.name);
});
});
});
}

Categories