Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting an object from observableArray by index

Tags:

knockout.js

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}
        ]
    }
]

}

like image 966
MrJM Avatar asked Jan 05 '13 17:01

MrJM


People also ask

How do you slice an observable array?

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().

How do you make an empty array observable?

To clear an observableArray you can set the value equal to an empty array.

Which function is used to search or sort an observable 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.


2 Answers

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.

like image 62
jjperezaguinaga Avatar answered Sep 23 '22 04:09

jjperezaguinaga


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.

like image 30
Rustam Avatar answered Sep 20 '22 04:09

Rustam