Knockout JS not updating when changing array - javascript

I know this was asked multiple times already, but none of that answered my question.
I have the following:
I get data over JSON to Javascript into a two dimensional array.
When I load the site, the table shows up like wanted.
Now when I click a button (just for testing), it is updating one value from the array and logging that array in the console, where I see the changed array.
The problem is that the change is not showing up in the table.
When I just add a value to the array, it is showing up in the table.
What am I doing wrong?
HTML:
<table id="sortsTable">
<tbody data-bind="foreach: sorts">
<tr>
<td data-bind="text: $data.name"></td>
<td data-bind="text: $data.ingName"></td>
</tr>
</tbody>
</table>
<button data-bind="click: addPerson">Add</button>
JS:
var sorts = ko.observableArray([]);
$(function() {
var request = new XMLHttpRequest();
var formData = new FormData();
var responseElements = [];
request.open("POST", "scripts.php", true);
formData.append("action", "getSorts");
request.onreadystatechange = function() {
if (request.readyState == 4 && request.status == 200) {
responseElements = JSON.parse(request.responseText);
sorts = convertList(responseElements);
ko.applyBindings(new AppViewModel(sorts));
}
}
request.send(formData);
});
function convertList(response) { //just the function to convert the json object to a more useful array
var names = [];
var ingredients = [];
var sorts = [];
for (var index = 0; index < response.length; index++) {
var name = response[index]['name'];
var ing = response[index]['ingName'];
if (names.indexOf(name) == -1) {
names.push(name);
}
if (ingredients.indexOf(ing) == -1) {
var nameIndex = names.indexOf(name);
if (ingredients[nameIndex] == undefined) {
ingredients[nameIndex] = ing;
} else {
ingredients[nameIndex] = ingredients[nameIndex] + ", " + ing;
}
}
}
for (var i = 0; i < names.length; i++) {
sorts[i] = {};
sorts[i]['name'] = names[i];
sorts[i]['ingName'] = ingredients[i];
}
return sorts;
}
function AppViewModel(data) {
var self = this;
self.sorts = data;
self.addPerson = function() {
console.log("click");
self.sorts[0]["name"] = "test"; //doesn't update table
//self.sorts.push({name: "qwer", ingName: "we"}); //works like expected
console.log(self.sorts);
};
}
Thanks.

The observable array only monitors which items are added to it, not the items themselves, so
self.sorts.push({name: "qwer", ingName: "we"}); //works like expected
works because you're getting the observable array to add to it's items, but
self.sorts[0]["name"] = "test"; //doesn't update table
doesn't work because the observable array has no way of knowing that an item inside it has changed. For this to work the properties of the items in the array will need to be observable.
In convertList switch to:
for (var i = 0; i < names.length; i++) {
sorts[i] = {};
sorts[i]['name'] = ko.observable(names[i]);
sorts[i]['ingName'] = ko.observable(ingredients[i]);
}
And they must be set by calling the observable setter method like so:
self.addPerson = function() {
console.log("click");
self.sorts[0]["name"]("test");
...
Also as an aside, you seem to have some other issues here. You define sorts as an observable array on the first line, but you overwrite it with the return value of convertList which is a normal array, not an observable one.
sorts = convertList(responseElements);
ko.applyBindings(new AppViewModel(sorts));
I'd remove the first line and create sorts as an observable array
function convertList(response) { //just the function to convert the json object to a more useful array
var names = [];
var ingredients = [];
var sorts = ko.observableArray([]);
...

The issue is that when you bind this:
<td data-bind="text: $data.name"></td>
$data.name is not observable, it's a simple property on an object, created here:
sorts[i]['name'] = names[i];
Knockout will quite happily bind properties like this, and display them, but any updates to them are not visible to knockout. Instead, as well as your observableArray, you also need to make any individual properties you want the ability to update observable as well:
sorts[i]['name'] = ko.observable(names[i]);
Then when you update it, knockout will see the change. Note however that you can't simply just assign to the property, as you'll just overwrite the knockout observable and it will be lost, instead you need to call the observable with the update:
self.sorts[0]["name"]("test");

Related

JavaScript - Issues recovering a map in an object after being saved in localStorage

I've been dealing with this for some time. I've a list of sections in which the user checks some checkboxes and that is sent to the server via AJAX. However, since the user can return to previous sections, I'm using some objects of mine to store some things the user has done (if he/she already finished working in that section, which checkboxes checked, etc). I'm doing this to not overload the database and only send new requests to store information if the user effectively changes a previous checkbox, not if he just starts clicking "Save" randomly. I'm using objects to see the sections of the page, and storing the previous state of the checkboxes in a Map. Here's my "supervisor":
function Supervisor(id) {
this.id = id;
this.verif = null;
this.selections = new Map();
var children = $("#ContentPlaceHolder1_checkboxes_div_" + id).children().length;
for (var i = 0; i < children; i++) {
if (i % 2 == 0) {
var checkbox = $("#ContentPlaceHolder1_checkboxes_div_" + id).children()[i];
var idCheck = checkbox.id.split("_")[2];
this.selections.set(idCheck, false);
}
}
console.log("Length " + this.selections.size);
this.change = false;
}
The console.log gives me the expected output, so I assume my Map is created and initialized correctly. Since the session of the user can expire before he finishes his work, or he can close his browser by accident, I'm storing this object using local storage, so I can change the page accordingly to what he has done should anything happen. Here are my functions:
function setObj(id, supervisor) {
localStorage.setItem(id, JSON.stringify(supervisor));
}
function getObj(key) {
var supervisor = JSON.parse(localStorage.getItem(key));
return supervisor;
}
So, I'm trying to add to the record whenever an user clicks in a checkbox. And this is where the problem happens. Here's the function:
function checkboxClicked(idCbx) {
var idSection = $("#ContentPlaceHolder1_hdnActualField").val();
var supervisor = getObj(idSection);
console.log(typeof (supervisor)); //Returns object, everythings fine
console.log(typeof (supervisor.change)); //Returns boolean
supervisor.change = true;
var idCheck = idCbx.split("_")[2]; //I just want a part of the name
console.log(typeof(supervisor.selections)); //Prints object
console.log("Length " + supervisor.selections.size); //Undefined!
supervisor.selections.set(idCheck, true); //Error! Note: The true is just for testing purposes
setObj(idSection, supervisor);
}
What am I doing wrong? Thanks!
Please look at this example, I removed the jquery id discovery for clarity. You'll need to adapt this to meet your needs but it should get you mostly there.
const mapToJSON = (map) => [...map];
const mapFromJSON = (json) => new Map(json);
function Supervisor(id) {
this.id = id;
this.verif = null;
this.selections = new Map();
this.change = false;
this.selections.set('blah', 'hello');
}
Supervisor.from = function (data) {
const id = data.id;
const supervisor = new Supervisor(id);
supervisor.verif = data.verif;
supervisor.selections = new Map(data.selections);
return supervisor;
};
Supervisor.prototype.toJSON = function() {
return {
id: this.id,
verif: this.verif,
selections: mapToJSON(this.selections)
}
}
const expected = new Supervisor(1);
console.log(expected);
const json = JSON.stringify(expected);
const actual = Supervisor.from(JSON.parse(json));
console.log(actual);
If you cant use the spread operation in 'mapToJSON' you could loop and push.
const mapToJSON = (map) => {
const result = [];
for (let entry of map.entries()) {
result.push(entry);
}
return result;
}
Really the only thing id change is have the constructor do less, just accept values, assign with minimal fiddling, and have a factory query the dom and populate the constructor with values. Maybe something like fromDOM() or something. This will make Supervisor more flexible and easier to test.
function Supervisor(options) {
this.id = options.id;
this.verif = null;
this.selections = options.selections || new Map();
this.change = false;
}
Supervisor.fromDOM = function(id) {
const selections = new Map();
const children = $("#ContentPlaceHolder1_checkboxes_div_" + id).children();
for (var i = 0; i < children.length; i++) {
if (i % 2 == 0) {
var checkbox = children[i];
var idCheck = checkbox.id.split("_")[2];
selections.set(idCheck, false);
}
}
return new Supervisor({ id: id, selections: selections });
};
console.log(Supervisor.fromDOM(2));
You can keep going and have another method that tries to parse a Supervisor from localStorageand default to the dom based factory if the localStorage one returns null.

How to update JavaScript array dynamically

I have an empty javascript array(matrix) that I created to achieve refresh of divs. I created a function to dynamically put data in it. Then I created a function to update the Array (which I have issues).
The Data populated in the Array are data attributes that I put in a JSON file.
To better undertand, here are my data attributes which i put in json file:
var currentAge = $(this).data("age");
var currentDate = $(this).data("date");
var currentFullName = $(this).data("fullname");
var currentIDPerson = $(this).data("idPerson");
var currentGender = $(this).data("gender");
Creation of the array:
var arrayData = [];
Here is the function a created to initiate and addind element to the Array :
function initMatrix(p_currentIDPerson, p_currentGender, p_currentFullName, p_currentDate, p_currentAge) {
var isFound = false;
// search if the unique index match the ID of the HTML one
for (var i = 0; i < arrayData.length; i++) {
if(arrayData[i].idPerson== p_currentIDPerson) {
isFound = true;
}
}
// If it doesn't exist we add elements
if(isFound == false) {
var tempArray = [
{
currentIDPerson: p_currentIDPerson,
currentGender: p_currentGender,
currentFullName: p_currentFullName,
currentDate: p_currentDate, currentAge: p_currentAge
}
];
arrayData.push(tempArray);
}
}
The update function here is what I tried, but it doesn't work, maybe I'm not coding it the right way. If you can help please.
function updateMatrix(p_currentIDPerson, p_currentGender, p_currentFullName, p_currentDate, p_currentAge) {
for (var i = 0; i < arguments.length; i++) {
for (var key in arguments[i]) {
arrayData[i] = arguments[i][key];
}
}
}
To understand the '$this' and elm: elm is the clickableDivs where I put click event:
(function( $ ) {
// Plugin to manage clickable divs
$.fn.infoClickable = function() {
this.each(function() {
var elm = $( this );
//Call init function
initMatrixRefresh(elm.attr("idPerson"), elm.data("gender"), elm.data("fullname"), elm.data("date"), elm.data("age"));
//call function update
updateMatrix("idTest", "Alarme", "none", "10-02-17 08:20", 10);
// Définition de l'evenement click
elm.on("click", function(){});
});
}
$('.clickableDiv').infoClickable();
}( jQuery ));
Thank you in advance
Well... I would recommend you to use an object in which each key is a person id for keeping this list, instead of an array. This way you can write cleaner code that achieves the same results but with improved performance. For example:
var myDataCollection = {};
function initMatrix(p_currentIDPerson, p_currentGender, p_currentFullName, p_currentDate, p_currentAge) {
if (!myDataCollection[p_currentIDPerson]) {
myDataCollection[p_currentIDPerson] = {
currentIDPerson: p_currentIDPerson,
currentGender: p_currentGender,
currentFullName: p_currentFullName,
currentDate: p_currentDate,
currentAge: p_currentAge
};
}
}
function updateMatrix(p_currentIDPerson, p_currentGender, p_currentFullName, p_currentDate, p_currentAge) {
if (myDataCollection[p_currentIDPerson]) {
myDataCollection[p_currentIDPerson] = {
currentGender: p_currentGender,
currentFullName: p_currentFullName,
currentDate: p_currentDate,
currentAge: p_currentAge
};
}
}
Depending on your business logic, you can remove the if statements and keep only one function that adds the object when there is no object with the specified id and updates the object when there is one.
I think the shape of the resulting matrix is different than you think. Specifically, the matrix after init looks like [ [ {id, ...} ] ]. Your update function isn't looping enough. It seems like you are trying to create a data structure for storing and updating a list of users. I would recommend a flat list or an object indexed by userID since thats your lookup.
var userStorage = {}
// add/update users
userStorage[id] = {id:u_id};
// list of users
var users = Object.keys(users);

Angular Check All button

I have a handful of simple checkbox lists that I need to create for an application. I built a "Check All" button for my initial test and it worked beautifully. But, when I changed the code to fetch a subset of the list via a Node call, the list still appeared as expected, but the Check All functionality no longer did. In my initial test, the list was just an array of objects with "description" and "value" but after inserting Node into the middle, the objects also had a $$hashkey property. I'm not sure if this is the source of the problem, but if someone could take a look and tell me what's wrong, I'd appreciate it.
My HTML looks like this:
<div class="row">
<div class="col-md-3" id="semRushApiList_None">
<input type="checkbox" value="semRushCheckAll_None" name="semRushCheckAll_None" ng-click="toggleSemRushApiTypes_None()" /><strong>Check All</strong>
<div ng-repeat="apiCall in semRushApiTypes_None">
<input type="checkbox" name="selectedSemRushApiTypes_None[]" value="{{apiCall.apiName}}" ng-checked="selectedSemRushApiTypes_None.indexOf(apiCall) > -1" ng-click="toggleSemRushApiSelection_None(apiCall)" /> {{apiCall.description}}
</div>
</div>
</div>
My angular js looks like this:
$scope.semRushCheckAll_None = false;
$scope.semRushApiTypes_None = [];
fetchApiTypesByCategory("none").then(function(types){
$scope.semRushApiTypes_None = types;
});
$scope.selectedSemRushApiTypes_None = [];
$scope.toggleSemRushApiTypes_None = function() {
$scope.semRushCheckAll_None = !$scope.semRushCheckAll_None;
if ($scope.semRushCheckAll_None) {
$scope.selectedSemRushApiTypes_None = angular.copy($scope.semRushApiTypes_None);
} else {
$scope.selectedSemRushApiTypes_None = [];
}
};
$scope.toggleSemRushApiSelection_None = function(apiCall) {
var idx = $scope.selectedSemRushApiTypes_None.indexOf(apiCall);
if (idx > -1) {
$scope.selectedSemRushApiTypes_None.splice(idx, 1);
} else {
$scope.selectedSemRushApiTypes_None.push(apiCall);
console.log(JSON.stringify($scope.selectedSemRushApiTypes_None));
}
};
function fetchApiTypesByCategory(category) {
var deferred = $q.defer();
$http.get(rootApiUrl + "/fetchSemRushApiTypesByCategory?category=" + category).then(function(response) {
deferred.resolve(response.data);
}, function(response){
deferred.reject("Error: " + response.data);
});
return deferred.promise;
}
The node call looks like this:
server.route({
method:"GET",
path:"/fetchSemRushApiTypesByCategory",
handler:function(request,reply){
var q = Qs.parse(request.query);
return reply(factory.createApiTypeList(q["category"])).code(200);
}
});
and the factory looks like this:
exports.createApiTypeList = function(category) {
var result = [];
for (var i = 0; i < semRushApiJson.length; i++) {
if (semRushApiJson[i].category === category) {
var description = semRushApiJson[i].description;
var apiName = "";
for (var p = 0; p < semRushApiJson[i].params.length; p++) {
if (semRushApiJson[i].params[p].key == "type") {
apiName = semRushApiJson[i].params[p].value;
break;
}
}
result.push({
"description": description,
"apiName": apiName
});
}
}
return result;
};
Some simple console.log statements have proven that things are being populated as expected, with the exception of the $$hashkey property on the objects coming out of the Node call.
When I check the checkboxes individually, the selected array is populated with a value that doesn't have the $$hashkey and when I check the check all, the selected list gets all of the appropriate values including the $$hashkey, but the checkboxes do not get updated on the UI like they did before I moved the populating of the list to a Node call.
Can anyone tell me what I'm doing wrong here?
V

Javascript: loop through array

This is driving me crazy. I'm just trying to print out an array and it's not working. What am I missing? The results variable is returning "undefined" which much mean my for loop isn't working correctly. Everything else works properly, the console.log I have correctly displays the fields are added to the array.
// The list of accounts array.
var accountsArray = [];
function addAccount() {
// Take fields and put user data into varables.
var accountName = document.getElementById('accountName').value;
var accountBalance = document.getElementById('accountBalance').value;
var accountType = document.getElementById("accountType");
var accountTypeSelected = accountType.options[accountType.selectedIndex].text;
var accountCurrency = document.getElementById("accountCurrency");
var accountCurrencySelected = accountCurrency.options[accountCurrency.selectedIndex].text;
// Put these variables into the array.
accountsArray.push(accountName);
accountsArray.push(accountBalance);
accountsArray.push(accountTypeSelected);
accountsArray.push(accountCurrencySelected);
// Items added to the array, logged.
console.log('user added: ' + accountsArray);
}
function accountsListHtml() {
var results;
// Loop through the array
for (var i = 0; i < accountsArray.length; i++) {
results = accountsArray[i];
}
document.getElementById('accountsList').innerHTML = results;
}
Here's a link to all the files. It's an iOS web app using Framework7. Balance Pro
You are calling accountsListHtml() in body.onload. At that point accountsArray is empty.
I can't find any other possibility to call accountsListHtml() on that page you linked to.
Add one line inside function addAccount() and it will work:
function addAccount() {
/* vour code */
console.log('user added: ' + accountsArray);
accountsListHtml(); // add this line
}
Try changing results = accountsArray[i]; to results += accountsArray[i];.
Update
And initialize results with an empty string, for example :)
for (var i = 0; i < accountsArray.length; i++) {
results = accountsArray[i];
}
The statement in the for loop i.e. results = accountsArray[i]; overwrites the variable results evry loop run. You could change the statement to :
results += accountsArray[i].toString();
and initialise results to an empty string.
The following works for me: http://jsfiddle.net/95ztrmk3/13/
HTML:
<div id="accountsList"></div>
JS:
// The list of accounts array.
var accountsArray = [];
addAccount();
accountsListHtml();
function addAccount() {
// Take fields and put user data into varables.
var accountName = "John Doe";
var accountBalance = "500.00";
var accountTypeSelected = "Checking"
var accountCurrencySelected = "USD";
// Put these variables into the array.
accountsArray.push(accountName);
accountsArray.push(accountBalance);
accountsArray.push(accountTypeSelected);
accountsArray.push(accountCurrencySelected);
// Items added to the array, logged.
console.log('user added: ' + accountsArray);
}
function accountsListHtml() {
var results = [];
// Loop through the array
for (var i = 0; i < accountsArray.length; i++) {
results += accountsArray[i] + " ";
}
document.getElementById('accountsList').innerHTML = results;
console.log(results);
}
Assuming the input isn't malformed or otherwise weird. I made sure Javascript recognizes results is an empty array and not a string or something: var results = []

return from JS function

basic JS question, please go easy on me I'm a newb :)
I pass 2 variables to the findRelatedRecords function which queries other related tables and assembles an Array of Objects, called data. Since findRelatedRecords has so many inner functions, I'm having a hard time getting the data Array out of the function.
As it currently is, I call showWin inside findRelatedRecords, but I'd like to change it so that I can get data Array directly out of findRelatedRecords, and not jump to showWin
function findRelatedRecords(features,evtObj){
//first relationship query to find related branches
var selFeat = features
var featObjId = selFeat[0].attributes.OBJECTID_1
var relatedBranch = new esri.tasks.RelationshipQuery();
relatedBranch.outFields = ["*"];
relatedBranch.relationshipId = 1; //fac -to- Branch
relatedBranch.objectIds = [featObjId];
facSel.queryRelatedFeatures(relatedBranch, function(relatedBranches) {
var branchFound = false;
if(relatedBranches.hasOwnProperty(featObjId) == true){
branchFound = true;
var branchSet = relatedBranches[featObjId]
var cmdBranch = dojo.map(branchSet.features, function(feature){
return feature.attributes;
})
}
//regardless of whether a branch is found or not, we have to run the cmdMain relationship query
//the parent is still fac, no advantage of the parent being branch since cmcMain query has to be run regardless
//fac - branch - cmdMain - cmdSub <--sometimes
//fac - cmdMain - cmdSub <-- sometimes
//second relationship query to find related cmdMains
var relatedQuery = new esri.tasks.RelationshipQuery();
relatedQuery.outFields = ["*"];
relatedQuery.relationshipId = 0; //fac -to- cmdMain
relatedQuery.objectIds = [featObjId];
//rather then listen for "OnSelectionComplete" we are using the queryRelatedFeatures callback function
facSel.queryRelatedFeatures(relatedQuery, function(relatedRecords) {
var data = []
//if any cmdMain records were found, relatedRecords object will have a property = to the OBJECTID of the clicked feature
//i.e. if cmdMain records are found, true will be returned; and continue with finding cmdSub records
if(relatedRecords.hasOwnProperty(featObjId) == true){
var fset = relatedRecords[featObjId]
var cmdMain = dojo.map(fset.features, function(feature) {
return feature.attributes;
})
//we need to fill an array with the objectids of the returned cmdMain records
//the length of this list == total number of mainCmd records returned for the clicked facility
objs = []
for (var k in cmdMain){
var o = cmdMain[k];
objs.push(o.OBJECTID)
}
//third relationship query to find records related to cmdMain (cmdSub)
var subQuery = new esri.tasks.RelationshipQuery();
subQuery.outFields = ["*"];
subQuery.relationshipId = 2;
subQuery.objectIds = [objs]
subTbl.queryRelatedFeatures(subQuery, function (subRecords){
//subRecords is an object where each property is the objectid of a cmdMain record
//if a cmdRecord objectid is present in subRecords property, cmdMain has sub records
//we no longer need these objectids, so we'll remove them and put the array into cmdsub
var cmdSub = []
for (id in subRecords){
dojo.forEach(subRecords[id].features, function(rec){
cmdSub.push(rec.attributes)
})
}
var j = cmdSub.length;
var p;
var sub_key;
var obj;
if (branchFound == true){
var p1 = "branch";
obj1 = {};
obj1[p1] = [cmdBranch[0].Branches]
data.push(obj1)
}
for (var i=0, iLen = cmdMain.length; i<iLen; i++) {
p = cmdMain[i].ASGMT_Name
obj = {};
obj[p] = [];
sub_key = cmdMain[i].sub_key;
for (var j=0, jLen=cmdSub.length; j<jLen; j++) {
if (cmdSub[j].sub_key == sub_key) {
obj[p].push(cmdSub[j].Long_Name);
}
}
data.push(obj);
}
showWin(data,evtObj) <---this would go away
})
}
//no returned cmdRecords; cmdData not available
else{
p = "No Data Available"
obj = {}
obj[p] = []
data.push(obj)
}
showWin(data,evtObj) <--this would go away
})
})
}
I'd like to have access to data array simply by calling
function findRelatedRecords(feature,evt){
//code pasted above
}
function newfunct(){
var newData = findRelatedRecords(feature,evt)
console.log(newData)
}
is this possible?
thanks!
Edit
Little more explanation.....
I'm connecting an Object event Listener to a Function like so:
function b (input){
dojo.connect(obj, "onQueryRelatedFeaturesComplete", getData);
obj.queryRelatedFeatures(input);
console.log(arr) //<----this doesn't work
}
function getData(relatedFeatData){
var arr = [];
//populate arr
return arr;
}
So when obj.QueryRelatedFeatures() is complete, getData fires; this part works fine, but how to I access arr from function b ?
Post Edit Update:
Due to the way that this event is being hooked up you can't simple return data from it. Returning will just let Dojo call to the next method that is hooked up to onSelectionComplete.
When init runs it is long before findRelatedRecords will ever be executed/fired by the onSelectionComplete event of the well, which is why you were seeing undefined/null values. The only way to work with this sort of system is to either 1) call off to a method like you're already doing or 2) fire off a custom event/message (technically it's still just calling off to a method).
If you want to make this method easier to work with you should refactor/extract snippets of it to make it a smaller function but contained in many functions. Also, changing it to have only one exit point at the end of the findRelatedRecords method will help. The function defined inside of subTbl.queryRelatedFeatures() would be a great place to start.
Sorry, you're kind of limited by what Dojo gives you in this case.
Pre Edit Answer:
Just return your data out of it. Everywhere where there is a showWin call just use this return.
return {
data: data,
evtObj: evtObj
}
Then your newfunct would look like this.
function newfunct(){
var newData = findRelatedRecords(feature,evt);
console.log(newData);
console.log(newData.data);
console.log(newData.evtObj);
}
If you only need that "data" object, then change your return to just return data;.
Also, start using semicolons to terminate statements.

Categories

Resources