I am building a quiz app with Vue 3 and Bootstrap 4. The quiz can be navigated in both directions (there is a nextQuestion
method and a prevQuestion
method).
const quizApp = {
data() {
return {
isScoreVisible: false,
questionCount: 0,
selectedAnswer: "",
correctAnswers: [],
results: [{
question: 'The book "The Little Prince" was written by...',
correct_answer: "Antoine de Saint-Exupéry",
selectedAnswer: "",
incorrect_answers: [
"Miguel de Cervantes Saavedra",
"Jane Austen",
"F. Scott Fitzgerald"
]
},
{
question: "Which novel by John Grisham was conceived on a road trip to Florida while thinking about stolen books with his wife?",
selectedAnswer: "",
correct_answer: "Camino Island",
incorrect_answers: ["Rogue Lawyer", "Gray Mountain", "The Litigators"]
},
{
question: 'In Terry Pratchett\'s Discworld novel "Wyrd Sisters", which of these are not one of the three main witches?',
selectedAnswer: "",
correct_answer: "Winny Hathersham",
incorrect_answers: [
"Granny Weatherwax",
"Nanny Ogg",
"Magrat Garlick"
]
}
]
};
},
methods: {
nextQuestion() {
if (this.questionCount < this.results.length - 1) {
this.questionCount ;
}
// Clear selected answer as you advance through the questions
this.selectedAnswer = "";
},
prevQuestion() {
if (this.questionCount >= 1) {
this.questionCount--;
}
},
checkAnswer(answer) {
let answersList = document.querySelectorAll("ul.answers li");
answersList.forEach(function(item) {
item.removeAttribute("class");
});
// check if the clicked anwser is equal to the correct answer
this.selectedAnswer = answer;
if (answer == this.results[this.questionCount].correct_answer) {
// Update the correct answers arary (make sure thare are no duplicates)
if (this.correctAnswers.indexOf(answer) === -1) {
this.correctAnswers.push(answer);
}
// Add correct answer class
event.target.classList.add("text-white", "bg-success");
} else {
// Add incorrect answer
event.target.classList.add("text-white", "bg-danger");
}
},
showScore() {
this.isScoreVisible = true;
},
resetQuiz() {
this.questionCount = 0;
this.correctAnswers = [];
this.selectedAnswer = "";
this.isScoreVisible = false;
},
removeElementFromArray(arr, elm) {
return arr.filter((el) => el !== elm);
},
shuffle(arr) {
var len = arr.length;
var d = len;
var array = [];
var k, i;
for (i = 0; i < d; i ) {
k = Math.floor(Math.random() * len);
array.push(arr[k]);
arr.splice(k, 1);
len = arr.length;
}
for (i = 0; i < d; i ) {
arr[i] = array[i];
}
return arr;
}
},
computed: {
answers() {
let incorrectAnswers = this.results[this.questionCount].incorrect_answers;
let correctAnswer = this.results[this.questionCount].correct_answer;
// return all answers, shuffled
return this.shuffle(incorrectAnswers.concat(correctAnswer));
},
showScoreBtn() {
return (
this.questionCount 1 == this.results.length &&
this.selectedAnswer != ""
);
}
}
};
Vue.createApp(quizApp).mount("#quiz_app");
#quiz_app {
height: 100vh;
}
.container {
flex: 1;
}
.logo {
width: 30px;
}
.nav-item {
width: 100%;
}
.category-name {
font-size: 2rem !important;
font-weight: 400;
text-align: center;
}
.questions .card {
width: 100%;
min-height: 330px;
}
.questions .card-header {
padding-top: 1.25rem;
padding-bottom: 1.25rem;
}
.questions .card-footer {
padding-top: 0.7rem;
padding-bottom: 0.7rem;
}
.answers li {
cursor: pointer;
display: block;
padding: 7px 15px;
margin-bottom: 5px;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.1);
background: #fff;
}
.answers li:last-child {
margin-bottom: 0;
}
.answers li:hover {
background: #fafafa;
}
.pager {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
justify-content: space-between;
}
.pager li > a {
display: inline-block;
padding: 5px 10px;
text-align: center;
width: 100px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 999px;
text-decoration: none !important;
color: #fff;
}
.pager li > a.disabled {
pointer-events: none;
background-color: #9d9d9d !important;
}
.results a,
.results a:hover {
color: #fff;
font-weight: 500;
border-radius: 999px;
}
.results a .fas {
font-size: 85%;
margin-right: 3px;
}
@media (min-width: 768px) {
.nav-item {
width: auto;
}
.questions .card {
width: 67%;
}
}
@media (min-width: 992px) {
.questions .card {
width: 50%;
}
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://kit.fontawesome.com/3f9baea05e.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/vue@next"></script>
<div id="quiz_app" class="container d-flex flex-column justify-content-center my-3">
<h2 class="display-4 category-name">Books</h2>
<div class="questions d-flex flex-column align-items-center">
<div v-if="results.length" class="card shadow-sm" :class="{'bg-light': isScoreVisible}">
<div v-if="isScoreVisible" class="results my-auto text-center">
<h5 class="mb-3">Your result: {{correctAnswers.length}} / {{results.length}}</h5>
<a href="#" class="btn btn-md bg-dark" @click="resetQuiz">
<i class="fas fa-sync-alt"></i> Play again
</a>
</div>
<template v-else>
<div class="card-header bg-light h6">
{{results[questionCount].question}}
</div>
<div class="card-body">
<ul class="answers list-unstyled m-0">
<li v-for="answer in answers" :key="answer" @click="checkAnswer(answer)">
{{answer}}
</li>
</ul>
</div>
<div class="card-footer bg-white">
<ul class="pager">
<li><a href="#" @click="prevQuestion" class="bg-dark" :class="{'disabled' : questionCount == 0}">Previous</a></li>
<li class="d-flex align-items-center text-secondary font-weight-bold small">
Question {{questionCount 1}} of {{results.length}}
</li>
<li v-if="showScoreBtn">
<a href="#" class="bg-dark" @click="showScore">Score</a>
</li>
<li v-else>
<a href="#" class="bg-dark" :class="{'disabled' : selectedAnswer == ''}" @click="nextQuestion">Next</a>
</li>
</ul>
</div>
</template>
</div>
</div>
</div>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
The problem
If I navigate back and forth through the quiz questions, the answers that I have already selected are not conserved.
Questions
- Is there a reliable way to keep the given answers using only JavaScript or should I use an API that saves them in a database?
- If I need an API, would the Trivia API do the job (or does it only allow get requests)?
CodePudding user response:
You can store all selected answers in the application state as an array. Modify your field selectedAnswers
, make it selectedAnswers = []
. Then, when an answer gets selected, store that answer in the array, with the index corresponding to the question index.
Below I modified your code to accomplish just this. I don't know enough vue to figure out how to conditionally apply styling to the correct answer, so I just added a list item which says Your answer: ...
. Note that when you go back to the previous questions, your previous answers is saved.
It should be trivial to modify this code, so that instead of rendering each answer, it will render the already selected one with different css classes, to achieve what you're looking for.
const quizApp = {
data() {
return {
isScoreVisible: false,
questionCount: 0,
selectedAnswers: [],
correctAnswers: [],
results: [{
question: 'The book "The Little Prince" was written by...',
correct_answer: "Antoine de Saint-Exupéry",
selectedAnswer: "",
incorrect_answers: [
"Miguel de Cervantes Saavedra",
"Jane Austen",
"F. Scott Fitzgerald"
]
},
{
question: "Which novel by John Grisham was conceived on a road trip to Florida while thinking about stolen books with his wife?",
selectedAnswer: "",
correct_answer: "Camino Island",
incorrect_answers: ["Rogue Lawyer", "Gray Mountain", "The Litigators"]
},
{
question: 'In Terry Pratchett\'s Discworld novel "Wyrd Sisters", which of these are not one of the three main witches?',
selectedAnswer: "",
correct_answer: "Winny Hathersham",
incorrect_answers: [
"Granny Weatherwax",
"Nanny Ogg",
"Magrat Garlick"
]
}
]
};
},
methods: {
nextQuestion() {
if (this.questionCount < this.results.length - 1) {
this.questionCount ;
}
},
prevQuestion() {
if (this.questionCount >= 1) {
this.questionCount--;
}
},
checkAnswer(answer) {
let answersList = document.querySelectorAll("ul.answers li");
answersList.forEach(function(item) {
item.removeAttribute("class");
});
// check if the clicked anwser is equal to the correct answer
this.selectedAnswers[this.questionCount] = answer;
if (answer == this.results[this.questionCount].correct_answer) {
// Update the correct answers arary (make sure thare are no duplicates)
if (this.correctAnswers.indexOf(answer) === -1) {
this.correctAnswers.push(answer);
}
// Add correct answer class
event.target.classList.add("text-white", "bg-success");
} else {
// Add incorrect answer
event.target.classList.add("text-white", "bg-danger");
}
},
showScore() {
this.isScoreVisible = true;
},
resetQuiz() {
this.questionCount = 0;
this.correctAnswers = [];
this.selectedAnswers = [];
this.isScoreVisible = false;
},
removeElementFromArray(arr, elm) {
return arr.filter((el) => el !== elm);
},
shuffle(arr) {
var len = arr.length;
var d = len;
var array = [];
var k, i;
for (i = 0; i < d; i ) {
k = Math.floor(Math.random() * len);
array.push(arr[k]);
arr.splice(k, 1);
len = arr.length;
}
for (i = 0; i < d; i ) {
arr[i] = array[i];
}
return arr;
}
},
computed: {
answers() {
let incorrectAnswers = this.results[this.questionCount].incorrect_answers;
let correctAnswer = this.results[this.questionCount].correct_answer;
// return all answers, shuffled
return this.shuffle(incorrectAnswers.concat(correctAnswer));
},
showScoreBtn() {
return (
this.questionCount 1 == this.results.length &&
this.selectedAnswers.length > 0
);
}
}
};
Vue.createApp(quizApp).mount("#quiz_app");
#quiz_app {
height: 100vh;
}
.container {
flex: 1;
}
.logo {
width: 30px;
}
.nav-item {
width: 100%;
}
.category-name {
font-size: 2rem;
font-weight: 400;
text-align: center;
}
.questions .card {
width: 100%;
min-height: 330px;
}
.questions .card-header {
padding-top: 1.25rem;
padding-bottom: 1.25rem;
}
.questions .card-footer {
padding-top: 0.7rem;
padding-bottom: 0.7rem;
}
.answers li {
cursor: pointer;
display: block;
padding: 7px 15px;
margin-bottom: 5px;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.1);
background: #fff;
}
.answers li:last-child {
margin-bottom: 0;
}
.answers li:hover {
background: #fafafa;
}
.pager {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
justify-content: space-between;
}
.pager li > a {
display: inline-block;
padding: 5px 10px;
text-align: center;
width: 100px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 999px;
text-decoration: none !important;
color: #fff;
}
.pager li > a.disabled {
pointer-events: none;
background-color: #9d9d9d !important;
}
.results a,
.results a:hover {
color: #fff;
font-weight: 500;
border-radius: 999px;
}
.results a .fas {
font-size: 85%;
margin-right: 3px;
}
@media (min-width: 768px) {
.nav-item {
width: auto;
}
.questions .card {
width: 67%;
}
}
@media (min-width: 992px) {
.questions .card {
width: 50%;
}
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://kit.fontawesome.com/3f9baea05e.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/vue@next"></script>
<div id="quiz_app" class="container d-flex flex-column justify-content-center my-3">
<h2 class="display-4 category-name">Books</h2>
<div class="questions d-flex flex-column align-items-center">
<div v-if="results.length" class="card shadow-sm" :class="{'bg-light': isScoreVisible}">
<div v-if="isScoreVisible" class="results my-auto text-center">
<h5 class="mb-3">Your result: {{correctAnswers.length}} / {{results.length}}</h5>
<a href="#" class="btn btn-md bg-dark" @click="resetQuiz">
<i class="fas fa-sync-alt"></i> Play again
</a>
</div>
<template v-else>
<div class="card-header bg-light h6">
{{results[questionCount].question}}
</div>
<div class="card-body">
<ul class="answers list-unstyled m-0">
<li v-for="answer in answers" :key="answer" @click="checkAnswer(answer)">
{{answer}}
</li>
<li>
Your answer: {{selectedAnswers[this.questionCount]}}
</li>
</ul>
</div>
<div class="card-footer bg-white">
<ul class="pager">
<li><a href="#" @click="prevQuestion" class="bg-dark" :class="{'disabled' : questionCount == 0}">Previous</a></li>
<li class="d-flex align-items-center text-secondary font-weight-bold small">
Question {{questionCount 1}} of {{results.length}}
</li>
<li v-if="showScoreBtn">
<a href="#" class="bg-dark" @click="showScore">Score</a>
</li>
<li v-else>
<a href="#" class="bg-dark" :class="{'disabled' : typeof selectedAnswers[this.questionCount] == undefined}" @click="nextQuestion">Next</a>
</li>
</ul>
</div>
</template>
</div>
</div>
</div>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
CodePudding user response:
I have gone through your code and quiz behavior as it looks you are not setting the value of selectedAnswer in this object.When you are calling checkAnswer also set selectAnswer that answer in that object.
{ question: 'In Terry Pratchett's Discworld novel "Wyrd Sisters", which of these are not one of the three main witches?', selectedAnswer: "", correct_answer: "Winny Hathersham", incorrect_answers: [ "Granny Weatherwax", "Nanny Ogg", "Magrat Garlick" ] }
If you face any issue in understanding and implementing this. You can contact me :)