Knockout observableArray is not binding correctly - javascript

I am in the process of learning about knockout/json/mvc et al and have tried to put together an example project, but for some reason I am unable to get the data to bind correctly.
In the code snippet below, I get some JSON data from a web server and then try to map it to a function and then eventually to my knockout observableArray. What I then do is use this observableArray to bind to a HTML table. However the HTML table is not displaying any data. I put a label on to the HTML page and this does print out but with a toString() value of :
[Object object]
five times, which matches the amount of properties in the JSON data.
Can anyone see anything obvious I am missing?
JSON received from web server:
{ "Id": 1, "Name": "Inst123", "Price": 10, "DateTime": "2014-01-16T17:22:43.6383507+00:00", "Description": "Descriptions" };
.
ViewModel
$(document).ready(function () {
var gtViewModel = new GvTradeViewModel();
ko.applyBindings(gtViewModel);
console.log("ViewModel created");
});
var GvTradeViewModel = function () {
var self = this;
self.gvTrades = ko.observableArray([]);
var apiUrl = "http://localhost:57858/api/Trade/1";
console.log("Starting JSON data retrieval from: " + apiUrl);
$.getJSON(apiUrl)
// Handle success event.
.done(function (jsonData) {
if (jsonData.isEmptyObject)
console.log("NoData recieved");
else {
console.log("JSON data: " + jsonData);
var mappedTrades = $.map(jsonData, function (gvTradeData) {
return new GvTrade(gvTradeData);
});
self.gvTrades(mappedTrades);
console.log(self.gvTrades);
}
})
// Handle error/fail event.
.fail(function (jqxhr, textStatus, error) {
var err = textStatus + ", " + error;
console.log("Request Failed: " + err);
});
};
function GvTrade(data) {
this.TradeId = ko.observable(data.TradeId);
this.InstrumentName = ko.observable(data.InstrumentName);
this.DateTime = ko.observable(data.DateTime);
this.Price = ko.observable(data.Price);
this.Description = ko.observable(data.Description);
}
HTML
<table>
<thead>
<tr>
<th>TradeId</th>
<th>InstrumentName</th>
<th>Price</th>
<th>DateTime</th>
<th>Description</th>
</tr>
</thead>
<tbody data-bind="foreach: $data.gvTrades">
<tr>
<td data-bind="text: InstrumentName"></td>
<td data-bind="text: Price"></td>
<td data-bind="text: DateTime"></td>
<td data-bind="text: Description"></td>
</tr>
</tbody>

The JSON coming from your server represents a single object and not an array.
So when you are calling $.map then it does not correctly maps your data as an array, so you will end up some unusable objects.
To fix this you need to make sure that your jsonData containing an array before the map operation, you can do this with calling jQuery.makeArray on it (or you can have an if which is based on your data type decide whether you need to map or not):
var mappedTrades = $.map($.makeArray(jsonData), function (gvTradeData) {
return new GvTrade(gvTradeData);
});
Demo JSFiddle.

Related

Angular - ng-repeat not updating on update of nested array

I am rendering data using ng-repeat through GET request, which retrieves an array.
HTML
<div ng-controller="candidateCtrl" >
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>NO</th>
<th>NAMA</th>
<th>NIP</th>
<th>INSTANSI</th>
<th><span ng-show="animateCandidateAdmin" class="ion-load-a"></span></th>
</tr>
</thead>
<tbody ng-repeat="candidate in candidatesAdmin">
<tr class="well whel">
<td>{{$index + 1}}</td>
<td>{{candidate.candidate_name}}</td>
<td>{{candidate.candidate_nip}}</td>
<td>{{candidate.candidate_institusi}}</td>
<td>
<button class="btn btn-xs btn-success" ng-show="candidate.m_assesment_assesment_id == NULL" ng-click="addCandidate3(candidate.candidate_id)">
</td>
</tr>
</tbody>
</table>
</div><!-- OFF-MAINBAR -->
<div ng-repeat="item in percentage_penilaian" >
<div id="candidate_{{item.m_assesment_assesment_id}}" >
<div class="panel-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>NO</th>
<th>NAMA</th>
<th>NIP</th>
<th>INSTANSI</th>
<th>BOBOT</th>
<th>SKOR</th>
<th>NILAI</th>
<th><span ng-show="animateCandidateManagerial" class="ion-load-a"></span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="candidate in candidates[item.m_assesment_assesment_id]" class="well whel">
<td>{{$index + 1}}</td>
<td>{{candidate.candidate_name}}</td>
<td>{{candidate.candidate_nip}}</td>
<td>{{candidate.candidate_institusi}}</td>
<td>{{candidate.percentage}}%</td>
<td ng-show="candidate.skor != NULL">
<button ng-click="$eval(arrAddCandidate[percentage_penilaian[$parent.$index+1].m_assesment_assesment_id])(candidate.candidate_id)"><i class="ion-arrow-right-a"></i> Mengikuti {{percentage_penilaian[$parent.$index+1].assesment_name}}</button>
</td>
</tr>
</tbody>
</table>
</div><!-- OFF-MAINBAR -->
</div>
</div>
</div>
</div>
JS
<script>
var SITEURL = "<?php echo site_url() ?>";
var selectionApp = angular.module("selectionApp", ["isteven-multi-select"]);
selectionApp.controller('candidateCtrl', function ($scope, $http) {
$scope.candidates = [];
$scope.arrAddCandidate = [];
$scope.getPercentagePenilaian = function () {
var url = SITEURL + 'xxx/xxx/' + 14;
$http.get(url).then(function (response) {
$scope.percentage_penilaian = response.data;
for(var i in response.data){
$scope.arrAddCandidate[response.data[i].m_assesment_assesment_id] = "addCandidate"+response.data[i].m_assesment_assesment_id;
}
})
};
$scope.getCandidateAdmin = function () {
var url = SITEURL + 'api/get_candidate_admin/' + 14;
$http.get(url).then(function (response) {
$scope.candidatesAdmin = response.data;
})
};
$scope.get_3 = function () {
var url = SITEURL + 'xxx/xxx/3__14';
$http.get(url).then(function (response) {
$scope.$apply(function () {
$scope.candidates[3] = response.data;
// $scope.candidates.push(response.data);
});
})
};
$scope.addCandidate3 = function (id) {
$scope.animateCandidateAdmin = true;
var postData = $.param({
candidate_id: id,
assesment_id: 3 });
$.ajax({
method: "POST",
url: SITEURL + "xx/xxx/xxxxx",
data: postData,
success: function (response) {
if(response=='sukses'){
$scope.animateCandidateAdmin = false;
$scope.getCandidateAdmin();
$scope.get_3();
}
}
});
};
$scope.get_5 = function () {
var url = SITEURL + 'xx/xxx/5__14';
$http.get(url).then(function (response) {
$scope.$applyAsync(function () {
$scope.candidates[5] = response.data;
// $scope.candidates.push(response.data);
});
})
};
$scope.addCandidate5 = function (id) {
$scope.animateCandidateAdmin = true;
var postData = $.param({
candidate_id: id,
assesment_id: 5 });
$.ajax({
method: "POST",
url: SITEURL + "xx/xxx/xxxxx",
data: postData,
success: function (response) {
if(response=='sukses'){
$scope.animateCandidateAdmin = false;
$scope.getCandidateAdmin();
$scope.get_5();
}
}
});
};
angular.element(document).ready(function () {
$scope.getPercentagePenilaian();
$scope.get_3;
$scope.get_5;
});
});
</script>
Response from $scope.getCandidateAdmin
[{"candidate_id":"24","candidate_name":"contoh","candidate_nip":"12345","candidate_institusi":"Institusi A","selection_selection_id":"14"}]
Response $scope.getPercentagePenilaian
[{"id":"14","m_assesment_assesment_id":"3","percentage":"50"},
{"id":"15","m_assesment_assesment_id":"5","percentage":"10"}]
Response from $scope.get_3
[{"id":"43","selection_selection_id":"14","m_assesment_assesment_id":"3"
,"candidate_id":"24","m_candidate_id" :"1","candidate_name":"contoh","candidate_nip":"12345","candidate_institusi":"Institusi A","competency_skor":null}]
After I adding candidate, I believe that the $scope.candidates array is updated correctly, however the table in my view does not change. I don't know what i'm doing wrong.
Potential Issue
I think 'm_assesment_assesment_id' is stored as a String and not a Integer, which is the root of your problem.
[
{ "id":"14",
"m_assesment_assesment_id":"3", // Note: the numbers are wrapped in quotes
"percentage":"50"
},
{ "id":"15",
"m_assesment_assesment_id":"5", // JSON parsers will interpret these as Strings, NOT numbers
"percentage":"10"}
]
Background to Understanding Problem
Since the ng-repeat is using the item.m_assesment_assesment_id property, Angular resolves these as Strings. When this String is passed into the Array, the Array is interpreted as a JavaScript Object and not an array. For example:
// Both the following evaluate to the same thing
// Using dot accessor
item.m_assesment_assesment_id
// Using square brackets to access data through Object Notation
item["m_assesment_assesment_id"]
This is why JavaScript interprets your Array as an Object, using item.m_assesment_assesment_id as a key and not and index.
<!-- item.m_assesment_assesment_id will evaluate as a String as the JSON data specifies the String format, triggering Object evaluation, not Array evaluation -->
<tr ng-repeat="candidate in candidates[item.m_assesment_assesment_id]" class="well whel">
Here is some good overview of Array Evaluation:
https://www.w3schools.com/js/js_arrays.asp
Associative Arrays Many programming languages support arrays with
named indexes.
Arrays with named indexes are called associative arrays (or hashes).
JavaScript does not support arrays with named indexes.
In JavaScript, arrays always use numbered indexes.
Solutions
There are a couple of solutions. If you have access to manipulating the JSON formatting, I would recommend correcting the problem there:
{
"id":"14",
"m_assesment_assesment_id": 3, // No quote == Integer
"percentage":"50"
}
But this would require you to have access to the server code constructing the response and there may be architectural reasons for using a string.
Alternatively, you could change your ng-repeat:
<tr ng-repeat="candidate in candidates[parseInt(item.m_assesment_assesment_id)]" class="well whel">
However there is a performance loss as this requires more processing during Angular's $digest() event. This will not be significant for small datasets, but it is worth noting.
If you don't have access to changing the JSON formatting on the server and you don't want to put extra load on the $digest event, your control could process the response data from the AJAX calls. This is often the approach recommended by Angular documentation.
angular.module("selectionApp", ["isteven-multi-select"])
.controller('candidateCtrl', function ($scope, $http) {
$scope.candidates = [];
$scope.arrAddCandidate = [];
$scope.getPercentagePenilaian = function () {
var url = SITEURL + 'xxx/xxx/' + 14;
$http.get(url).then(function (response) {
// USE OUR CONVENIENCE METHOD HERE TO CONVERT STRINGS TO INTEGERS
$scope.percentage_penilaian = processPercentagePenilaian(response.data);
for(var i in response.data){
$scope.arrAddCandidate[response.data[i].m_assesment_assesment_id] = "addCandidate"+response.data[i].m_assesment_assesment_id;
}
})
};
// *** MORE of your code exists between these functions
// ALSO YOU HAVE TO CALL THIS IN THE CALLBACK OF THE APPROPRIATE AJAX CALLBACK
var processPercentagePenilaian = function(items) {
// Loop through the Items
angular.forEach(items, function(item, key) {
// For Every Item, convert the m_assesment_assesment_id to an Integer
item.m_assesment_assesment_id = parseInt(item.m_assesment_assesment_id);
});
// End Controller
});

Callback data is not being recognized

I am trying to delete this entire row whenever you click the Delete button. This is my jQuery command:
UPDATE: I have updated the click function to my finalized version:
$(document).on('click', '.delete-assignment',function () {
console.log("click");
var data = {
assignment_id: $(this).closest('tr').find('.assignment-id').html(),
class_id: $('#classId').val()
}
var row = $(this).closest('tr');
deleteAssignment(data, function(returnData){
var returnData = JSON.parse(returnData);
if(returnData.status == "Success"){
console.log("yes");
row.hide();
}
});
});
When I click delete, it triggers the deleteAssignment function successfully and returns a callback of {"status":"Success"}. Yet when I returnData.status == "Success" is not being triggered.If I try jQuery.type(returnData), It says string. So I implemented JSON.parse and it says unexpected token in json at position 0
here is my html:
<tbody id="Homework">
<tr>
<td>Homework Test Title</td>
<td>02/16/2017 - 10:00 AM</td>
<td class="assignment-id">51</td>
<td><button type="button" class="btn btn-danger delete-assignment">Delete</button></td>
</tr>
</tbody>
I wanted to also include how I am passing data back to deleteAssignment as a callback (defined in the javascript function (deleteAssignment)
assignment = Assignments.objects.get(id=data['assignment_id'])
assignment.delete()
data = {}
data['status'] = "Success"
return HttpResponse(json.dumps(data), content_type="application/json")
You have a clouser problem.
The variable this inside your callback function is not the same this that inside the click function.
There are several ways to solve this, here is one of them:
$('.delete-assignment').on('click', function () {
var data = {
assignment_id: $(this).closest('tr').find('.assignment-id').html(),
class_id: $('#classId').val()
}
var that = this;
deleteAssignment(data, function(returnData){
console.log(returnData);
if(returnData.status == "Success"){
print("yes");
$(that).closest('tr').remove();
}
});
});

How to update data from database with api call in angular

I have a api call who give me the list of data, and I am iterating data via ng-repeat (its a list of more than 100 items)
For getting list of data I have call an Api in App Controller in angularjs like this:
var path = serverUrl + 'api/getAllMails';
$http.get(path).then(function (result) {
$scope.mails=result
})
For Iterating the mails in Html file i have use table like the below
<table>
<tr class="header">
<th class="center">Id</th>
<th class="center">Mode of Payment</th>
<th class="center">Payment Collected</th>
<th class="center">Status</th>
</tr>
<tr ng-repeat="mail in mails">
<td>{{mail.id}}</td>
<td>{{mail.paymentType}}</td>
<td>Rs. {{mail.cost}}
<input type="text" ng-model="mail.cost">
<button ng-click="updateCost=(mail.id, mail.cost)">Update Cost</button>
</td>
<td>{{mail.status}}
<input type="text" ng-model="mail.status">
<button ng-click="updateStatus(mail.id, mail.status)">Update Status</button>
</td>
</tr>
</table>
Suppose in the first iterations the cost will be "100" and the status will be "pending". And I have to update this row only, change cost to "1000" and status will be "Delivered".
In my App controller of Angularjs I have create methods. These two methods are calling apis and updating data in database and return the list of updated mails.
$scope.updateStatus = function(mailId, mailStatus) {
var path = serverUrl + 'api/updateStatus';
$http.get(path, {
params: {
mailId: mailId,
mailStatus: mailStatus
}
}).then(function(result) {
$scope.mails = result
})
}
$scope.updateCost = function(mailId, mailCost) {
var path = serverUrl + 'api/updateStatus';
$http.get(path, {
params: {
mailId: mailId,
mailCost: mailCost
}
}).then(function(result) {
$scope.mails = result
})
}
These code are working fine but while it took lot of time to load a page. So what can I do to reduce the loading time or is there any better way to do the same thing.
Any help will be appreciable. Thank you
You are replacing the entire dataset when there is no reason for that, you should only update the row you change. Ensure your updateStatus return the object you update and update that item in $scope.mails
In example
$scope.updateCost = function(mailId, mailCost) {
var path = serverUrl + 'api/updateStatus';
$http.get(path, {
params: {
mailId: mailId,
mailStatus: mailCost
}
}).then(function(result) {
// result is the item you changed
for (var i = $scope.mails.length - 1; i >= 0; i--) {
if($scope.mails[i].id === mailId) {
$scope.mails[i] = result;
return;
}
};
})
}

Observable array not showing all items in array

I have a knockout observableArray bound to a table below.
<table id="Users">
<thead>
<tr>
<td>User Name</td><td>Primary Email</td><td>Product Role</td><td>Active</td><td>EditUser?</td>
</tr>
</thead>
<tbody data-bind="foreach: CustomerUsers">
<tr>
<td data-bind="text: UserName"></td>
<td data-bind="text: PrimaryEmail().EmailAddress"></td>
<td><select></select></td>
<td data-bind="text: StaticActiveText"></td>
<td>Edit User</td>
</tr>
</tbody>
</table>
And the following view model:
function CustomerAdminVm() {
var vm = this
this.CustomerUsers = ko.observableArray(GetCustomerUsers());
}
My problem is that when the view model initially loads GetCustomerUsers it correctly gets the values it needs, and inserts them into the observable array. However, the elements are not displayed in the table.
The strange thing is that if I call the following function:
this.AddUserToCustomer = function () {
if (vm.NewUser) {
vm.CustomerUsers.push(vm.ActiveUser());
}
vm.CloseUserModalDialog();
}
The user is added to the array, and correctly displayed in the table. Even more confusing is that the latest value of the array in the push shows the initial values that are not displayers are in the array.
Does anyone have any idea what could be causing this behavior?
The code for GetCustomerUsers is:
function GetCustomerUsers() {
var users = [];
$.ajax({
type: 'Get',
url: ControllerBase + 'Actions/GetAllUsersForCustomer',
async: false,
success: function (data) { users = $.map(data, function(item) { return new ObservableUser(item); }); }
});
return users;
}
And ActiveUser is also an ObservableUser.
What you should do is to create the ko variable in the constructor and then update it with the ajax values when the data arrives.
function CustomerAdminVm() {
var vm = this
this.CustomerUsers = ko.observableArray();
GetCustomerUsers(this.CustomerUsers);
}
function GetCustomerUsers(getData) {
$.ajax({
type: 'Get',
url: ControllerBase + 'Actions/GetAllUsersForCustomer',
async: false,
success: function (data) { getData($.map(data, function(item) { return new ObservableUser(item); })); }
});
}
Ok I actually found a work around for this. Instead of populating the data as part of the constructor I add it after the document is ready
function CustomerAdminVm() {
var vm = this
this.CustomerUsers = ko.observableArray();
$(document).read(function(){vm.CustomerUsers(GetCustomerUsers());});
}
I'm going to throw a theory out as to what is actually happening, feel free to tell me I am wrong, but the solution does work.
What I believe to be happening is the data is being populated in the viewmodel constructor. But because the call is a synchronous call the constructor is not completing before the data is being populated. then the apply bindings function is being called, but because the data is prepoulated before the apply bindings function comes back the data already in the object is ignored because it has no observable change. During a normal async operation the call the constructor completes, then the apply bindings function is applied, and then the data comes back and the observable is changed causing an event flag to update the view.

Creating table from API

My function is pulling information from an api and then putting it into the table. For some reason this function is not working.
this is my function:
function getPayInfo(socCode) {
var apiurl = "http://api.lmiforall.org.uk/api/v1/ashe/estimatePay?soc=";
var apicall = apiurl + socCode;
$.get(apicall, function(data) {
$("#Pay tbody").html("");
$.each(data.years, function(i, e) {
var tablerow = $("<tr></tr>");
tablerow.append("<td>" + e.year + "</td>");
tablerow.append("<td>" + e.estpay + "</td>");
$("#Pay tbody").append(tablerow);
});
});
}
and this is the table I am putting it into:
<div class="well table table-stripped">
<h4>Pay Information</h4>
<h5>Pounds per Week</h5>
<table id="Pay">
<thead>
<tr>
<th>Year</th>
<th>Estimate Pay</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
I expected it to return a year and data value into the table. if you take the url in the function and add a certain 4 figure value (the socCode) (e.g. 5113) to the end it will give the data online this should then be returned to the table body.
Have you debugged/examined the data structure inside your $.get callback? It looks to me that your access method is incorrect. When I access your example url, I get the following data back:
{
"soc":5113,
"series":[
{
"year":2012,
"estpay":340
}
]
}
I think you want your iterator to be this instead:
// there is no data.years
$.each(data.series, function(i, e) {
...
This should make your e param be of the form {year: 2012, estpay: 340}, which appears to be what you want to interpret it as.

Categories