How do I for-each over a Knockout.JS observableArray? - javascript

All attempts to iterate over a KO observableArray have failed. The flow just jumps over the block like the array is empty.
It's not, because its bound to some HTML and the debugger shows 7 items.
I've tried a normal for with an indexer, an ECMA-5+ forEach and now KO's own arrayForEach utility.
var EditorViewModel = function (firstDayOfWeek) {
this.firstDayOfWeek = firstDayOfWeek;
this.days = ko.observableArray([]); // Added in server-side generated view below.
// Reads the activity data from each day and constructs and uploads models.
this.save = function () {
var basket = [];
// Construct the upload activity models.
ko.utils.arrayForEach(this.days(), function(d) {
... // never falls into this block.
There's nothing much out on the web about this, so I guess its a no-brainer. I'm obviously messing it up somehow, but its eluding me this afternoon.
this.days array looks good to me.
Thanks, Luke

here days is a observableArray which is nothing but a function , to iterate you need to read the value like days() , this is will give you a javascript array.
var days = ko.observableArray([1,3,4,5,6]);
days().forEach(function(v,i){
alert(v);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

I don't know why its not working with the forEach methods, but I did get it going with a properly written traditional for index iterator.
for (dayIndex = 0; dayIndex < this.days().length; dayIndex++) { ... }

Related

I can console.log my array but it's values shows up as undefined when appended to a div

I can console.log the array before and after I step through it to build list items. When I run the code I get 10 list items in my html that all read "undefined" instead of being the values I'm pulling out of my chrome history.
Any ideas as to why?
var urlarray = []
var historyResults = function () {
var microsecondsPerWeek = 1000 * 60 * 60 * 24 * 7;
var oneWeekAgo = (new Date).getTime() - microsecondsPerWeek;
chrome.history.search({
'text': '', // Return every history item....
'startTime': oneWeekAgo // that was accessed less than one week ago.
}, function(historyItems) {
for (var i = 0; i < historyItems.length; ++i) {
urlarray.push(historyItems[i].url);
}
})
console.log(urlarray)
}
historyResults()
function addElement () {
var makeUL = function(data) {
var ul = document.createElement("ul");
// create the UL
console.log(urlarray)
for (i = 0; i < 10; i++) {
var a = document.createElement('a');
a.href = data[i];
a.appendChild(document.createTextNode(data[i]));
console.log(a)
var li = document.createElement("li");
li.appendChild(a);
ul.appendChild(li);
// step through the array and create a link out of the value of the array and append them to the list item
// append the list item to the UL
}
return ul;
// return ul full of li
}
console.log(urlarray)
document.getElementById("arraylist").appendChild(makeUL(urlarray));
// go to html to find div and append the full UL with urlarray as the array
}
addElement()
You have two issues going on.
First, you are logging the array, but your browser does not log it immediately. It does so when it has the CPU available. When you log the array, its not yet populated with values. A moment later, when you expand the array in your browser console, the array is now populated because the evaluation of the array is delayed.
You can see this more clearly if you change your logging statement to: console.log(JSON.stringify(urlarray)). This forces the immediate evaluation of the object and turns it into a JSON string, which can then be written to the browser console a moment later.
For more information on the delayed evaluation of logged objects, see this question.
Okay, this brings us to your second issue. Your logging statement is executing before the callback to chrome.history.search does. That's why the array isn't yet populated. You need to use promises to ensure your code executes in the expected sequence. For this you should use a library like jQuery or Q.
I recommend reading about promises. Whichever library you use, your code will follow this basic structure:
Get a 'deferred' object. I'll call it deferred.
In your callback, resolve deferred with the array: deferred.resolve(urlarray)
Where your logging statement is, get the promise from the deferred object. Return that promise from the historyResults method.
Where you call historyResults, instead do:
historyResults.then(function(urls) {
console.log("promise was resolved!", urls);
// do things with your urls here, like add elements
});
Do things that depend on your urls here, inside this callback. If you do, your code will be guaranteed to execute when the urls array is fully populated and ready to go.
This is a big topic, so google "javascript promises" and good luck. I hope this helps to get you started in the right direction.
If you don't want to use promises:
If you don't want to use promises, you will need to do everything inside the callback to chrome.history.search. That's the only way to guarantee the array is populated.
Asynchronous code is fun.

Protractor variable scope with promises

Background
I'm working on an Angular app which uses ng-repeat to make a table. One of the users found that the table sometimes contains duplicate entries, which I confirmed visually, then promptly wrote a Protractor test for.
The Test
Variable Scoping Issues
While writing the test, I noticed that the scope wasn't behaving in a way that I understood.
Naturally, the for-loop on line 61 has access to linkStorage (line 38), since it is in a higher scope. It logs that all of the objects have been successfully added to the object via the for-loop in the promise on line 47.
However, when I move the confirmation loop outside of the promise, say, before the expect block...
...linkStorage is an empty object.
Looping over the object finds no nested key-value pairs; it is truely empty.
Question (tl;dr)
Why is the linkStorage object populated inside the then statement, but not before the expectation?
Asynchronousity Strikes Again
The first example works is due to asynchronousity. Because the .getAttribute method is non-blocking, the code continues to run past it while it works. Therefore, the console loop is reached before the object has been populated; it's empty.
If you give the asynchronous code some time to run, maybe one second:
...linkStorage is populated.
Complete Solution
Chain multiple promises together to ensure code runs at the correct time.
it('should not have duplicates within the match grid', function() {
// Already on job A, with match grid shown.
var duplicate = false;
var linkStorage = {};
// Save unique links
var uniqueUserLinks = element.all(by.css('div.row table tbody tr td a'));
// get an array of href attributes
uniqueUserLinks.getAttribute('href')
.then(function(hrefs) {
// add the links to the linkStorage object
for (var i = 0; i < hrefs.length; i++) {
// if the link is already there
if( linkStorage[ hrefs[i] ] ) {
// update its counter
linkStorage[hrefs[i]] += 1
duplicate = true;
// there's already one duplicate, which will fail the test
break;
} else {
// create a link and start a counter
linkStorage[hrefs[i]] = 1;
}
};
}).then(function() {
// confirm links have been added to storage
for(var link in linkStorage) {
console.log('link:', link );
console.log('number:', linkStorage[link] );
}
}).then(function() {
expect(duplicate).toBe(false);
});
});

Why is observableArray not observable in Knockout JS?

I am new to programming (especially in JS and even more with KO) and I'm trying to come up with an interactive quiz destined to be used in class by high-school students. Thanks to this article (which provided the how-to and the code, which I used as a base, trying to readapt it to my needs) and some good people's help here, I have now come up with something that looks like this:
http://jsfiddle.net/sNJm3/2/
All is well because this is functional. But... :p I would now like to add an observableArray where I would push() all the selectedAnswer each time the user clicks one so that I could, at the end, compare selectedAnswers().length to questions().length and, if they are the same, I'd make a (not included in the code yet) visible.
I declared my array in the QuizViewModel constructor like so (as it concerns the whole quiz, so I think that's where it should go):
var selectedAnswers = ko.observableArray();
And then I need, each time, to push the selectedAnswer property from the Question constructor into it. And that's where the rub is... Here's the part of my script:
//Construction
$.each(quizName.controls, function(index, question) {
quiz.questions.push(new Question(index + 1, question));
quiz.selectedAnswers().push(question.selectedAnswer);
});
This does populate an array called selectedAnswers() but it is only populated with Undefineds, which 1) do not vary even when I click an answer (undefined is not replaced with clicked selectedAnswer...) and selectedAnswers().length is already equal to the total number of questions, which means the comparison I wanted to make will not work...
There must be some fundamental KO logic I'm not getting here (or is it JS logic, which definitely seems to be eluding me!) Any thoughts on the matter would be greatly appreciated!
Use a computed for your "selected answers" list.
function Question(config) {
this.text = text;
this.questionText = config.questionText;
this.answers = config.answers;
this.selectedAnswer = ko.observable();
}
function QuizViewModel(quizName) {
this.questions = ko.observableArray(
ko.utils.arrayMap(quizName.controls, function (control) {
return new Question(control);
})
);
this.selectedAnswers = ko.computed(function () {
return ko.utils.arrayMap(this.questions(), function (q) {
return q.selectedAnswer();
}
});
}
There is no need to maintain a separate stack (i.e. observable array) of answers when the answer already is a property of the question itself.

Variable Dependency with knockoutJS

I'm building an application with KnockoutJS with a component that essentially acts as a sequential spreadsheet. On different lines users may define variables or use them to represent a value.
So for example
x =2
x //2
x = 4
x //4
I have this working in the straightforward case of continuing adding new lines. The output function for each line checks and iterates backwards to see if the variable was ever defined previously. If it was it uses the first example it finds and sets that as the value. This works when initially defining the lines, and also works when you edit a line after a previous line has changed.
However, I would like variables to update if a previous definition of that variable has changed, been removed, or been added. That behavior does not exist right now. I have tried adding my own custom dependency handling code using a map to track the variables, but it badly impacted performance. I would like to tap into Knockouts dependency management to solve this, but I'm not sure of the best way to do so. Here is a brief summary of my code structure, I would be happy to add more detail if needed.
calcFramework is the view-model object I bind to the map. It consists of an observable list of Lines, a varMap, and other unrelated properties and functions
Line is a custom object. The relevant code is below
var Line = function (linenum,currline) {
var self = this;
self.varMap = {};
self.input = ko.observable("");
self.linenum = ko.observable(linenum);
self.lnOutput = ko.computed({
read:function(){
return outputFunction(self,self.input());
},
write:function(){},
owner:self
});
};
function outputFunction(self,input) {
try{
var out = EQParser.parse(input,10,self);
return out.toString();
}
catch(ex){
//error handling
}
}
Line.prototype.getVar = function (varName, notCurrentLine) {
if(typeof varName === "undefined"){
return null;
}
//Actually don't want ones set in the current varMap, only past lines
if(varName in this.varMap && notCurrentLine){
return this.varMap[varName];
}
if (this.linenum() > 0) {
var nextLine = calcFramework.lines()[this.linenum() - 1];
return nextLine.getVar(varName,true);
} else {
//eventually go to global
return calcFramework.varMap[varName];
}
};
Line.prototype.setVar = function(varName,value){
this.varMap[varName] = value;
};
SetVar and getVar are passed to eqParser, which gets the value of the expression, calling those functions as needed if a variable is referenced. So the variable value is not explicitly passed to the function and thus knockout does not view it as a dependency. But I'm not sure how I would pass the variable as a parameter without traversing the list every time.
So my question is, given this setup, what is the best way to track changes to a variable assignment (and/or new assignments) and update the lines that reference that variable, while maintaining good performance.
I recognize my question is lengthy and I have attempted to trim out all unnecessary detail. Thanks for your patience in reading.
I would be tempted to use a publish/subscribe model, using something like Peter Higgins' PubSub jquery plugin
Your overall app would subscribe/listen out for lines publishing an event that they have a variable definition. This would store any variable names in a standard javascript hashtable, along with the value. When a variable found event is published by a line, the app would check through all the known variables, and if it finds that it is a change to an existing variable value, it would publish a variable changed event. All the lines would subscribe to that event. They can then check whether they have a variable matching that name, and update the value accordingly.
Here's some untested code to give you an idea of what I mean:
var app = function()
{
var self = this;
self.variables = {};
$.subscribe('/variableAssigned', function (key, value)
{
// I think that this is the best way of checking that there is a variable
// in the object
if(self.variables.hasOwnProperty(key))
{
if(self.variables[key] !== value)
{
$.publish('/variableChanged', [ key, value ]);
}
}
});
}
In your Line object:
$.subscribe('/variableChanged', function (key, value)
{
// loop through varMap and see if any of them need updating.
});

Google Docs - spreadsheet loop

This one might be a bit basic and easy to answer, but I've been pulling out my hair for a while now!
I've built the following code - which is semi-pseudo as I can't find the right way to make things work!
var s = "Test";
function onEdit(event)
{
var ss = event.source.getActiveSheet();
var r = event.source.getActiveRange();
if (ss.getName() == s)
{
results = {"Currently On": 0, "Next Up": 0, "On Hold": 0, "Waiting on someone else": 0, "zDone": 0};
last = ss.getMaxRows();
start = ss.getRange("F3:"+last).getValues();
var output = "J11";
for (x=0;x<start.length;x++)
{
results[start[x]]++;
}
for (y=0;y<results.length;y++)
{
row = ss.getRow(output);
row.value = results[y];
output++;
}
}
}
I've got an example of the data in this image
The basic idea is to run through all the possible categories of each task and keep a numeric list on the side of how many of each there are. I'd also like to make it dynamic (so I don't have to hard code in the list of categories) but I'm more interested in just making it work for the moment.
The Google Apps debugger is very frustrating!
Thanks for your help all!
Firstly, this particular use case would be easily achievable with a spreadsheet formula, eg:
=QUERY(A2:F;"select F, count(A) where F != '' group by F label count(A) 'Count'";1)
but there may be a reason why you want to do this with GAS.
So secondly, this is where I think there may be some syntax issues:
last = ss.getMaxRows();
I would just use var last = ss.getLastRow() here.
start = ss.getRange("F3:"+last).getValues();
The range reference would evaluate to something like "F3:100", which is a valid reference in GSheets (don't know about whether GAS can handle it), but nevertheless you really want something like "F3:F100", so I would use var start = ss.getRange("F3:F"+last).getValues();.
results[start[x]]++;
When you create an array from a getValues() call it is a 2D array, so you would need to use results[start[x][0]]++;.
With the next loop and the output variable, I must admit I'm a bit lost with what you're doing there. How did you want your result table laid out?
You have
output = "J11";
And then you do
ss.getRow(output);
output++;
This is invalid.First of all, ss is a Sheet under which there is not getRow method. So, what you should really be doing is something like this
var row = 11 ;
var col = 10 ; //Col J
for (y=0;y<results.length;y++)
{
ss.getRange(row,col,1,1).setValue(results[y]);
row++:
}
Like AdamL, I suggest that this is better handled within the native capability of the spreadsheet. Seems to me that you want a pivot table, which would update dynamically. Alternatively a formula like =countif(F:F,"Currently On" ) would meet your immediate request. =Unique(F:F) will give you the list of categories in an array

Categories

Resources