0

I'm creating a Quiz app on Meteor. So in a helper function of mine, I created an array of all the ids from the different documents (all equal to the different questions + answers) in the database and set it equal to a variable.

var queue = Quiz.find().fetch();
      for(var i = 1; i < queue.length; i++){
        if(queue[i]){
          result = queue[i]._id;
          break;
        }

      }

      Router.go("question", {_id: result});

What I want to accomplish is that I want the router to receive a new id after each iteration of the loop. So after every time a user answers a question, I want a new id to come up so the user gets a new question and not the old one.

I am thinking about using the .pop() method, displaying the last _id of the array and removing it from the array.

My question is, will using .pop() completely remove the entry from the database? As I'm writing this right now, I think it will since the cursors are reactive. Any way around this?

8
  • Perhaps do a single call to the Quiz collection getting a bunch of quiz questions in a random configuration. Then you'd save this, in order, to a collection that is for the user, perhaps an QuizTaker collection. The collection would have the username and an array of the questions to take, at least. Commented Sep 29, 2015 at 17:25
  • it depends, what if the user leaves and comes back to your site, if you store the collection on the server (MongoDB) that is unique to the user, then they could continue where they left off Commented Sep 29, 2015 at 17:27
  • Yeah. I meant this app to be just a one visit, take quiz, leave type of thing. Just a project to get a hang of Meteor. Commented Sep 29, 2015 at 17:30
  • Popping from a local fetched array will not be reactive. It's not the same as doing a Collection.remove() Commented Sep 29, 2015 at 17:34
  • 1
    ah so I'm safe to pop the fetched data without the risk of removing it? nice! I would try before asking, but don't want to risk removing anything. Commented Sep 29, 2015 at 17:47

1 Answer 1

2

I'll try to structure the problem:

  1. You provide a (random) list of numbers to the client, each number is a question ID.
  2. You need to manipulate this list on certain events originated by user.
  3. You need to maintain the app state corresponding to this list.
  4. And you need to show different things according to certain (head) element of the list.

It is actually easier to manipulate collections than mapping arbitrary numbers to collections' entries. So the workflow changes slightly:

  1. You provide a collection, sorted and reduced somehow, to the client.
  2. The client should remember certain (head) element of the exposed collection.
  3. And it should render corresponding DOM according to that element of the exposed collection.

Now, step by step.

Expose the collection

Let's say you have a collection:

Quiz = new Mongo.Collection('quiz');

Quiz.insert({
    published: true,
    question: 'The tallest waterfall on the planet',
    answers: [{
        id: 'A',
        label: 'Niagara Falls'
    }, {
        id: 'B',
        label: 'Angel Falls'
    }, {
        id: 'C',
        label: 'Victoria Falls'
    }],
    correct: 'B'
});

What you do next is you publish it on the server:

Meteor.publish('quiz', function () {
    return Quiz.find({
        published: true // this will filter out all the "unpublished" questions if there are any
    }, {
        fields: {
            correct: 0 // this will filter out the field so it doesn't appear on client
        }
    });
});

And subscribe to it on the client:

Meteor.subscribe('quiz');

From this moment, every time you perform find or findOne on Quiz collection on the client, it will yield only published questions omitting correct property.

Remember the state

The simplest way to remember the state is to store it within session variable. So first thing, you should set it and then change its value according to whether a user answered correctly or not:

// Initiate questionsAnswered session variable, an array of answered questions
var questionsAnswered = Session.get('questionsAnswered') || [];
Session.set('questionsAnswered', questionsAnswered);

// Pick a quasi-random question that hasn't been answered yet
var question = Quiz.findOne({
    question: {
        $nin: Session.get('questionsAnswered')
    }
});

// When user answers the question
Session.set('questionsAnswered', questionsAnswered.concat(question.question));

I think I have to clarify a bit.

  1. Session variable questionsAnswered stores an array of strings, each string is literally a question, i.e. a question property of the Quiz collection's entry.
  2. $nin in find query means "not in (list)" and works this way: if you have a collection with some IDs [1, 2, 3, 4] then "not in this list" will return every entry that has ID not equal to 1, 2, 3 or 4.
  3. findOne guarantee that only one (or zero) entry is being selected from a collection. It is a shorthand for find().fetch()[0].
  4. concat, since the session variable stores an array, is a method of Array that concatenates two arrays or an array with a scalar value, and returns the result. Or, in this particular case, an array of string with a single string.

Render it

Now, it's time to render it. Remember, the current question is stored within local variable question. You can put it into another session variable and access it from the template helper, so the code is:

Session.set('currentQuestion', question);

Template.theQuestion.helpers({
    question: function () {
        return Session.get('currentQuestion');
    }
});

And the template may look like this:

<template name="theQuestion">
    <p class="question">{{ question }}</p>
    <ul class="answers">
        {{#each answers}}
            <li class="answer">
                <label for="{{ id }}">
                    <input type="radio" value="{{ id }}">
                    {{ label }}
                </label>
            </li>
        {{/each}}
    </ul>
</template>

Push the next question into the router state

Summarizing all this, what you need is, for each question, do

Router.go('question', question);

where question is current question (head element of collection cursor). This will make URL represent current question, and the whole thing will manage to move through the list of questions, one by one, at the same time having the list of answered questions stored within the session variable, as well as the current question.

I say "current" but it can be easily perceived as "next".

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you so much for your clear explanation. The only slight difference in my case is that my quiz loads a "new page" after every question. So after answering a question, the user clicks "next" and another question loads. The router I have uses the :_id parameter, so I'm trying to figure out how to randomly choose the id so that when it is answered, it's removed and doesn't appear again and the same question doesn't show again. But how you did it here makes much more sense. Thank you!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.