I'll try to structure the problem:
- You provide a (random) list of numbers to the client, each number is a question ID.
- You need to manipulate this list on certain events originated by user.
- You need to maintain the app state corresponding to this list.
- 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:
- You provide a collection, sorted and reduced somehow, to the client.
- The client should remember certain (head) element of the exposed collection.
- 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.
- 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.
$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.
findOne guarantee that only one (or zero) entry is being selected from a collection. It is a shorthand for find().fetch()[0].
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".