I'm designing a quiz with Knockoutjs. I want to display only one question at a time. I was thinking about a global count var which is added by 1 each time a question is answered. However I can't seem to find a way to get only one question at the time. How would I best approach this? I'm new to Knockoutjs. I tried questions()[number] but that didn't seem to work.
Thanks
JS
$(document).ready(function(){
function Answer(data) {
this.answer = ko.observable(data.answer);
this.correct = ko.observable(data.correct);
}
function Question(data){
this.id = ko.observable(data.id);
this.question = ko.observable(data.question);
this.answers = ko.observableArray([]);
var mappedAnswers = $.map(data.answers, function(item) {return new Answer(item) });
this.answers(mappedAnswers);
}
function AnswerViewModel(){
// Data
var self = this;
self.questions = ko.observableArray([]);
self.checkAnswer = function(item, event){
if (item.juist() == true){
$(event.currentTarget).css("background", "green");
}else{
$(event.currentTarget).css("background", "red");
}
}
$.getJSON("../res/json/quiz.json", function(allData) {
var mappedQuestions = $.map(allData.questions, function(item) {return new Question(item) });
self.questions(mappedQuestions);
});
}
ko.applyBindings(AnswerViewModel());
});
HTML
<h3 data-bind="questions()[1].question"></h3>
<ul class="list-container" data-bind="foreach: questions()[1].answers">
<li class="list-item">
<a class="list-item-content" href="#" data-bind="text: answer, click:checkAnswer"></a>
</li>
</ul>
<button>Next question</button>
JSON
{
"questions" :
[
{
"id": 1,
"question": "This is the first question",
"answers" :
[
{"answer": "q1a1", "correct": false},
{"answer": "q1a2", "correct": false} ,
{"answer": "q1a3", "correct": true},
{"answer": "q1a4", "correct": false}
]
},
{
"id": 2,
"question": "This is the 2nd question",
"answers" :
[
{"answer": "q2a1", "correct": false},
{"answer": "q2a2", "correct": false} ,
{"answer": "q2a3", "correct": false},
{"answer": "q2a4", "correct": true}
]
}
]
}
The slice function is the observableArray equivalent of the native JavaScript slice function (i.e., it returns the entries of your array from a given start index up to a given end index). Calling myObservableArray. slice(...) is equivalent to calling the same method on the underlying array (i.e., myObservableArray().
To clear an observableArray you can set the value equal to an empty array.
The KnockoutJS Observable sort() method sorts all items in the array. By default, items are sorted in an ascending order. For sorting an array in a descending order, use reverse() method on sorted array.
Jsfiddle is being funny so I can't post the code there, but it's quite simple: you were almost there! Here's a little push.
First, is not really good practice to call the methods in the markup unless necessary, and giving parameters to the function probably means you can use an observable for that. In this case:
<div data-bind="foreach: filteredQuestions">
<ul class="list-container" data-bind="foreach: answers">
<li class="list-item">
<a class="list-item-content" href="#" data-bind="text: answer"></a>
</li>
</ul>
<button data-bind="click: $parent.nextQuestion">Next question</button>
</div>
As you can see, in the markup I'm only putting the variable, not calling it out in the form filteredQuestions
. By now you may be asking, "What's up with that foreach? I only want to display one question, not all of them. And what's up with the filtered thing?"
Here's what I'm doing: instead of displaying the original questions array, I'm displaying a filtered one, that gets updated every time any observables inside it changes. Here's the code for the filteredQuestions.
self.currentQuestionId = ko.observable("1");
self.filteredQuestions = ko.computed(function() {
var currentQuestion = self.currentQuestionId();
return ko.utils.arrayFilter(self.question(), function(question) {
return question.id() == currentQuestion;
});
});
If you pay attention to the code, I'm only returning one array with one question only, the one which Id is the same as the one set up by the currentQuestionId
variable. This is one approach, although there are many: other good one has an observable called selectedQuestion
which value is the first question; then in the markup use the data binding with:selectedQuestion
. But let's stick with this one, can you guess what does $parent.nextQuestion
does?
self.nextQuestion = function() {
var currentQuestionId = self.currentQuestionId();
self.currentQuestionId(currentQuestionId + 1);
}
First, we use the $parent
keyword in order to browse the actual scope parent, since we are in the children question
because of the foreach
statement. This function works because of your requirements, but there may be some pitfalls there (what do we do in the last question? what if the id's are not numerically increasing?). That's when the other approach might be more useful, in which in this case you would have something like this:
self.nextQuestion = function(question) {
var index = self.questions().indexOf(question);
self.selectedQuestion(self.questions()[index + 1]);
}
The beauty of the last approach? Well, backQuestion is quite easy!
self.backQuestion = function(question) {
var index = self.questions().indexOf(question);
self.selectedQuestion(self.questions()[index - 1]);
}
(You obviously need to check IndexOutOfBounds type errors). In this last approach, don't forget that since we are inside a context switching element like foreach
, we receive the current question as a parameter. Hope this helps.
Here is a working solution.
The code uses mapping plugin to simplify view model creation.
"questionIndex" and "currentQuestion" observables tracks current question.
function AnswerViewModel(){
var self = this;
ko.mapping.fromJS(data, {}, self);
this.questionIndex = ko.observable(0);
this.currentQuestion = ko.computed(function(){
return self.questions()[self.questionIndex()];
});
this.nextQuestion = function(){
var questionIndex = self.questionIndex();
if(self.questions().length - 1 > questionIndex){
self.questionIndex(questionIndex + 1);
}
};
this.prevQuestion = function(){
var questionIndex = self.questionIndex();
if(questionIndex > 0){
self.questionIndex(questionIndex - 1);
}
};
this.checkAnswer = function(item, event){
$(event.currentTarget).css("background", item.correct() ? "green" : "red");
};
}
Markup:
<!-- ko with: currentQuestion -->
<h3 data-bind="text: question"></h3>
<ul class="list-container" data-bind="foreach: answers">
<li class="list-item">
<a class="list-item-content" href="#" data-bind="text: answer, click:$root.checkAnswer"></a>
</li>
</ul>
<!-- /ko -->
Here I use virtual element <!-- ko --> <!-- /ko -->
to introduce desired context.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With