Vue-js remember check box choices for every question - javascript

I'm making a quiz app. I have question selection menu and I need to save each checkbox state for each question. For example, if you were to select an answer for 1st question, I need to save your choices even if you skip to 3rd question so when you come back to 1st your old choices are still there.
Now the problem that I have is - my checkboxes aren't unique, they are same for each question. Please take a look at gif on what happens.
https://i.imgur.com/hua2NI8.gif
This is my code
<template>
<div v-if="loading"><loader></loader></div>
<div v-else class="main">
<h5 class="test-title">Test</h5>
<h5 class="choosequestion">
<font-awesome-icon icon="list-ul" style="margin-right: 8px" />Choose question
</h5>
<div v-dragscroll="true" class="questionsbox">
<!-- answered, current -->
<input v-for="(question, index) in questions" :key="index" :value="index + 1" #click="chooseQuestion(index)" class="questionbox" type="button" />
</div>
<div class="buttons">
<a class="ctlBtn">Restart</a>
<a class="ctlBtn">End</a>
</div>
<div class="questionmain">
<h5 class="questiontext">
<font-awesome-icon icon="question-circle" style="margin-right: 8px" />
{{ currentQuestion.question }}
</h5>
<ul>
<li
v-for="( answer, key ) in currentQuestion.answers"
:index="currentQuestion.key"
:key="answer.title"
>
<input v-model="checkedAnswers[key]" #change="answerClick" :id="answer.title" type="checkbox" />
<label :for="answer.title" >{{ answer.title }}</label>
</li>
{{ checkedAnswers }}
</ul>
<img
class="questionimg"
:src="currentQuestion.image_url"
/>
<a class="nextBtn" #click="nextQuestion"><font-awesome-icon icon="arrow-right" style="color: black;"/></a>
</div>
</div>
</template>
<script>
import { dragscroll } from "vue-dragscroll";
import Loader from "./Loader.vue";
export default {
components: { Loader },
name: "TestCard",
data() {
return {
questions: [],
loading: true,
index: 0,
checkedAnswers: []
};
},
computed: {
currentQuestion() {
if (this.questions !== []) {
return this.questions[this.index];
}
return null;
},
},
methods: {
async getQuestions() {
this.loading = true;
let response = await fetch("http://localhost:4000/test");
let jsonResponse = await response.json();
// put data on questions property
this.questions = jsonResponse.questions;
this.loading = false;
},
answerClick() {
console.log("answer clicked");
},
chooseQuestion(index) {
this.index = index;
},
nextQuestion() {
this.index += 1;
console.log("next question")
}
},
mounted() {
this.getQuestions();
},
directives: {
dragscroll,
},
};
</script>
After that, I would like to check if answers are correct when user presses on "End". How do I connect each checkbox to it's answer and question, then check it if its correct? I have "correct: true" in JSON for each correct answer :)
Thank you

Without changing too much of your existing code you could go about mapping your answers to questions like this. No need for Vuex or more complexity.
<template>
...
<ul>
<li
v-for="( answer, key ) in currentQuestion.answers"
:index="currentQuestion.key"
:key="answer.title"
>
// we pass the key to answerClick to be able to map the answer to the current question
<input v-model="checkedAnswers[key]" #change="answerClick(key)" :id="answer.title" type="checkbox" />
<label :for="answer.title" >{{ answer.title }}</label>
</li>
{{ checkedAnswers }}
...
</template>
</template>
<script>
export default {
name: "TestCard",
data() {
return {
questions: [],
loading: true,
index: 0,
checkedAnswers: [],
answers: []
};
},
computed: {
currentQuestion() {
if (this.questions !== []) {
return this.questions[this.index];
}
return null;
}
},
methods: {
async getQuestions() {
this.loading = true;
let response = await fetch("http://localhost:4000/test");
let jsonResponse = await response.json();
// put data on questions property
this.questions = jsonResponse.questions;
this.loading = false;
},
answerClick(key) {
console.log("answer clicked");
if (!this.answers[this.index]) this.answers[this.index] = [] // if the current question has no answers mapped yet set an empty array
this.answers[this.index][key] = this.checkedAnswers[key]; // set the answer to whatever value it has currently (true, false)
},
chooseQuestion(index) {
this.index = index;
this.checkedAnswers = this.answers[this.index] ? this.answers[this.index] : []; // set the checkedAnswers to what we can find in our mapping, if there is nothing to find use an empty array
},
nextQuestion() {
this.index += 1;
this.checkedAnswers = []; // unset the checked answers for the next question
console.log("next question")
}
},
mounted() {
this.getQuestions();
},
};

Related

How do I watch the output of a function?

I have 2 buttons. One adds a movie to local storage, the other removes it from there. I made a function that basically switches the button. If the movie is added it shows "remove", if the movie's not been added it shows the button "add".
The function works but it doesn't know when the boolean changes so the button doesn't change. Someone explained that i should use watch property, but how am I supposed to watch an output of a function?
here is the code
<template>
<div>
<div class="card" v-for="movie in movies"
:key="movie.id">
{{movie.title}}
{{movie.release_date}}
<button v-show="!showButton(movie.id)" type="submit" #click="storeMovie(movie.id)" >
Aggiungi
</button>
<button v-show="showButton(movie.id)" type="submit" #click="removeMovie(movie.id)">
Rimuovi
</button>
</div>
<div class="card" v-for="favourite in watchlist"
:key="favourite.id">
{{favourite.title}}
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'HomeComp',
data () {
return {
movies: [],
watchlist: [],
movie: null,
}
},
mounted () {
axios
.get('https://api.themoviedb.org/3/movie/popular?api_key=###&language=it-IT&page=1&include_adult=false&region=IT')
.then(response => {
this.movies = response.data.results
// console.log(response.data.results)
})
.catch(error => {
console.log(error)
this.errored = true
})
.finally(() => this.loading = false)
},
watch: {
switchButton(oldValue, newValue) {
if (oldValue != newValue) {
this.showButton(id) = true;
} //made an attempt here
}
},
methods: {
storeMovie(id) {
const favouriteMovie = this.movies.find(movie => movie.id === id )
this.watchlist.push(favouriteMovie);
localStorage.setItem("watchlist", JSON.stringify(this.watchlist));
},
removeMovie(id) {
const removedMovie = this.watchlist.find(movie => movie.id === id )
const indexMovie = this.watchlist.indexOf(removedMovie);
if (indexMovie > -1) {
this.watchlist.splice(indexMovie, 1);
}
localStorage.setItem("watchlist", JSON.stringify(this.watchlist));
},
showButton(id) {
const favouriteMovie = this.watchlist.find(movie => movie.id === id )
if (favouriteMovie && favouriteMovie.length > 0) {
return true
} else{
return false
}
}
},
}
</script>
<style scoped lang="scss">
</style>
A better approach would be to store the state of a movie being stored or not in the watchlist directly on the movie object.
Then use a computed to get the watchlist from the movie list instead of using two different arrays.
<template>
<div>
<div class="card" v-for="movie in movies" :key="movie.id">
{{movie.title}}
{{movie.release_date}}
<button v-show="!movie.toWatch" type="submit" #click="storeMovie(movie.id)">
{{ movie.toWatch ? 'Rimuovi' : 'Aggiungi' }}
</button>
</div>
<div class="card" v-for="favourite in watchList" :key="favourite.id">
{{favourite.title}}
</div>
</div>
</template>
<script>
export default {
name: 'HomeComp',
data() {
return {
movies: [],
}
},
computed: {
// Get the watchList from the movies list
watchList() {
return this.movies.filter(movie => movie.toWatch)
}
},
watch: {
watchList(newWatchList) {
// Update the localStorage whenever the list changes
localStorage.setItem("watchlist", JSON.stringify(newWatchList));
}
},
mounted() {
// your axios call
},
methods: {
storeMovie(id) {
const favouriteMovie = this.movies.find(movie => movie.id === id)
if (favouriteMovie) {
// just reverse the boolean
favouriteMovie.toWatch = !favouriteMovie.toWatch
}
},
},
}
</script>

how can i do a call to axios wtihout fall in a infinite loop

i did a netflix copy with two different Axios call for series and films... Now i want add an Axios call for actors... and i did it... but i fall in an infinite loop, so after one minute the page crash... do you know why???
This is a CardFilm.vue that is repeated with a v-for in MainFilm.vue...
In my card i ve got all Film info from film's API... i want add a section for actors, so i m taking another API for actors... i had back 20 objects where i take just the element.name (actor name), with this.arrayAttori.length i take just the first 5 element of the array ACTORS of API, but after that it work i fall in an infinite loop, because my code ask infinite time the ACTORS ARRAY of API
TEMPLATE
<template>
<div class="card p-4 col-2 text-center text-white bg-black">
<img
v-show="filmData.poster_path != null"
:src="'http://image.tmdb.org/t/p/w342/' + filmData.poster_path"
:alt="filmData.title"
/>
<h3>{{ filmData.title }}</h3>
<h5
v-show="
filmData.title.toLowerCase() != filmData.original_title.toLowerCase()
"
>
{{ filmData.original_title }}
</h5>
<lang-flag
class="flag"
:iso="filmData.original_language"
:squared="false"
/>
<h4>{{ filmData.vote_average }}</h4>
<div>
<div class="star" v-for="element in fullArrayStars()" :key="element">
&starf;
</div>
<div
class="actor"
v-for="element in ricercaAttori /*()*/"
:key="element.name"
>
{{ element.name }}
</div>
</div>
</div>
</template>
<script>
import LangFlag from "vue-lang-code-flags";
import axios from "axios";
export default {
name: "CardBool",
data() {
return {
starf: "star",
arrayStars: [],
stars: Math.floor(this.filmData.vote_average / 2),
arrayAttori: null,
};
},
components: {
LangFlag,
},
props: {
filmData: Object,
},
methods: {
fullArrayStars() {
return this.stars;
},
ricercaAttori() {
axios
.get(
`https://api.themoviedb.org/3/movie/${this.filmData.id}/credits?api_key=9631e84004e35c8371fcb3c009af9551`
)
.then((response) => {
this.arrayAttori = response.data.cast;
});
this.arrayAttori.length = 5;
console.log(this.arrayAttori);
return this.arrayAttori;
},
},
};
</script>
i m working in VUE CLI...
thanks to everyone
methods: {
fullArrayStars() {
return this.stars;
},
changeVisibilita() {
if (this.visibilita == false) {
this.visibilita = true;
} else {
this.visibilita = false;
}
},
},
created() {
axios
.get(
`https://api.themoviedb.org/3/movie/${this.filmData.id}/credits?api_key=9631e84004e35c8371fcb3c009af9551`
)
.then((response) => {
this.arrayAttori = response.data.cast;
})
.then(() => {
this.arrayAttori.splice(5, this.arrayAttori.length);
});
return this.arrayAttori;
},
this is the solution... you have to use created() so you can do the call to the API before the creation of the Card, and you have to use then two times...
one time to create the array and the second time to work in that array, in this case to splice it

Vue js: How to hide button

i'm new to Vue js in this code below , i wanted to hide button "Clear Filter" when nothing selected and show the button only when function " selectedAnswer(index)" called so that it will show only when filter applied otherwise it should be hided , is there a way to do it in my code?
and thanks in advance
<template>
<div class="container" width=800px>
<b-row>
<b-col cols="8">
<h1> Recently Asked </h1>
<ul class="container-question" v-for="(question1,index) in questions" :key="index">
<li>
{{question1.question}}
</li>
</ul>
</b-col>
<b-button class="outline-primaryy" style="margin:auto;" #click="ClearFilter" >Clear Filter</b-button>
</div>
<router-view />
</div>
</template>
<script>
export default {
data() {
return {
questions: [],
answered: null,
index: 0,
selectedIndex: null,
}
},
methods: {
selectedAnswer(index) {
this.selectedIndex = index;
this.questions = this.questions.filter((question) => question.incorrect_answers.includes(index))
console.log(index)
},
ClearFilter() {
this.questions = this.unmutated
},
watch: {
question1: {
handler() {
this.selectedIndex = null;
this.answered = false;
},
},
},
},
mounted: function() {
fetch('https://opentdb.com/api.php?amount=10&category=9&difficulty=medium&type=multiple', {
method: 'get'
})
.then((response) => {
return response.json()
})
.then((jsonData) => {
this.questions = jsonData.results
this.unmutated = jsonData.results;
})
}
}
</script>
You just need to add a v-if="selectedIndex" to your btn element.
ie
<b-button v-if="selectedIndex" class="outline-primaryy" style="margin:auto;" #click="ClearFilter" >Clear Filter</b-button

Vue v-for loop click event affecting all items

I've searched and couldn't see any answer that fits what I need. I have a v-for loop with a button on each item and used VueClipboard2 to copy text. Anytime the button is clicked, I do some css changes to indicated the item that was copied. What happens is that, if there's more than 1 item, clicking on any button affects affect every other item and does the same effect.
I want to limit the clicking to the "own" item being clicked.
Here's my code:
<template>
<div class="form" id="shorten">
<form class="" #submit.prevent="shortener($event, value)">
<div>
<div class="form__shortener">
<input
class="form-input"
type="url"
name="link"
id="link"
placeholder="shorten a url here"
aria-label="input a url"
v-model="value"
/>
<button class="form-btn btn">
{{ buttonText }}
<p v-if="loading" class="loading"></p>
</button>
</div>
<SlideXLeftTransition :delay="100">
<p v-if="error" class="error">Please enter a valid link</p>
</SlideXLeftTransition>
</div>
</form>
<SlideYUpTransition group>
<div v-for="(link, index) in links" :key="index" class="form__links">
<p class="form__links-main">
{{ link.mainUrl }}
</p>
<div class="center form__links-copy">
<p>
<a :href="link.shortenedUrl" class="form__links-copy-link no-decoration">{{ link.shortenedUrl }}</a>
</p>
<button
class="form__links-copyBtn btn"
:class="[copied === true ? 'copied' : '']"
v-clipboard:copy="link.shortenedUrl"
v-clipboard:success="onCopy"
v-clipboard:error="onError"
>
<span v-if="!loading && !copied">Copy</span>
<span v-if="copied">Copied!</span>
</button>
</div>
</div>
</SlideYUpTransition>
</div>
</template>
<script>
import { required, minLength } from 'vuelidate/lib/validators';
import { SlideYUpTransition, SlideXLeftTransition } from 'vue2-transitions';
import axios from 'axios';
export default {
data() {
return {
value: '',
links: [],
message: '',
error: false,
loading: false,
buttonText: 'Shorten it!',
shortenedUrl: '',
copied: false,
};
},
validations: {
value: {
required,
minLength: minLength(1),
},
},
methods: {
async shortener(event, value) {
this.$v.$touch();
if (this.$v.$invalid) {
this.showError();
} else {
try {
this.loading = true;
this.buttonText = 'Loading';
const request = await axios.post('https://rel.ink/api/links/', { url: value });
this.loading = false;
this.buttonText = 'Shortened!';
setTimeout(() => {
this.buttonText = 'Shorten it!';
}, 1200);
this.shortenedUrl = `https://rel.ink/${request.data.hashid}`;
const mainUrl = request.data.url.length <= 20 ? request.data.url : `${request.data.url.slice(0, 30)}...`;
this.links.push({
shortenedUrl: `https://rel.ink/${request.data.hashid}`,
mainUrl,
});
localStorage.setItem('links', JSON.stringify(this.links));
} catch (error) {
this.showError();
console.log(error);
}
}
},
onCopy() {
this.copied = true;
setTimeout(() => {
this.copied = false;
}, 2500);
},
showError() {
this.error = true;
setTimeout(() => {
this.error = false;
}, 2000);
},
onError() {
alert('Sorry, there was an error copying that link. please reload!');
},
getLinks() {
if (localStorage.getItem('links')) this.links = JSON.parse(localStorage.getItem('links'));
},
},
components: {
SlideYUpTransition,
SlideXLeftTransition,
},
mounted() {
this.getLinks();
},
};
</script>
I would appreciate if anyone who help out.
Here's the live link: https://url-shortener-vue.netlify.app
To replicate, shorten two lines and click on the copy button on 1. It triggers all other items button.
Thank you.
Reason for your problem is
:class="[copied === true ? 'copied' : '']". SInce when you click any copy button, you change copied, and same class is used in all the iterations.
So, got the problem.
Solution is, you should have this copied corresponding to each link. So make your link as object.
link = [{ link: 'url...', copied: false}, {}, ...].
and, check for each link's copied value.

Click-and-edit text input with Vue

I'm looking for a click-and-edit Vue component.
I've found a fiddle and made some edits. It works like this:
The fiddle is here.
The problem: I need an additional click to make the input focused. How can I make it focused automatically?
The code from the fiddle. HTML:
<div id="app">
Click the values to edit!
<ul class="todo-list">
<li v-for = "todo in todos">
<input v-if = "todo.edit" v-model = "todo.title"
#blur= "todo.edit = false; $emit('update')"
#keyup.enter = "todo.edit=false; $emit('update')">
<div v-else>
<label #click = "todo.edit = true;"> {{todo.title}} </label>
</div>
</li>
</ul>
</div>
JS:
new Vue({
el: '#app',
data: {
todos: [{'title':'one value','edit':false},
{'title':'one value','edit':false},
{'title':'otro titulo','edit':false}],
editedTodo: null,
message: 'Hello Vue.js!'
},
methods: {
editTodo: function(todo) {
this.editedTodo = todo;
},
}
})
You can use a directive, for example
JS
new Vue({
el: '#app',
data: {
todos: [
{ title: 'one value', edit: false },
{ title: 'one value', edit: false },
{ title: 'otro titulo', edit: false }
],
editedTodo: null,
message: 'Hello Vue.js!'
},
methods: {
editTodo: function (todo) {
this.editedTodo = todo
}
},
directives: {
focus: {
inserted (el) {
el.focus()
}
}
}
})
HTML
<div id="app">
Click the values to edit!
<ul class="todo-list">
<li v-for="todo in todos">
<input
v-if="todo.edit"
v-model="todo.title"
#blur="todo.edit = false; $emit('update')"
#keyup.enter="todo.edit=false; $emit('update')"
v-focus
>
<div v-else>
<label #click="todo.edit = true;"> {{todo.title}} </label>
</div>
</li>
</ul>
</div>
You can find more info here
https://v2.vuejs.org/v2/guide/custom-directive.html
With #AitorDB's help I have written a Vue component for this, I call it Click-to-Edit. It is ready to use, so I'm posting it.
What it does:
Supports v-model
Saves changes on clicking elsewhere and on pressing Enter
ClickToEdit.vue: (vue 2.x)
<template>
<div>
<input type="text"
v-if="edit"
:value="valueLocal"
#blur.native="valueLocal = $event.target.value; edit = false; $emit('input', valueLocal);"
#keyup.enter.native="valueLocal = $event.target.value; edit = false; $emit('input', valueLocal);"
v-focus=""
/>
<p v-else="" #click="edit = true;">
{{valueLocal}}
</p>
</div>
</template>
<script>
export default {
props: ['value'],
data () {
return {
edit: false,
valueLocal: this.value
}
},
watch: {
value: function() {
this.valueLocal = this.value;
}
},
directives: {
focus: {
inserted (el) {
el.focus()
}
}
}
}
</script>
Edit for 3.x: [Breaking changes between 2.x and 3.x]
remove .native from the event handlers
change the focus hook to mounted as described in Custom Directives 3.x.
Built on #Masen Furer's work. I added some protection to handle when a user deletes all of the data. There is probably a way to accomplish this using "update" but I couldn't get it working.
I also added the ability to hit escape and abandon any changes.
<template>
<span>
<input type="text"
v-if="edit"
:value="valueLocal"
#blur="save($event);"
#keyup.enter="save($event);"
#keyup.esc="esc($event);"
v-focus=""/>
<span v-else #click="edit = true;">
{{valueLocal}}
</span>
</span>
</template>
<script>
export default {
props: ['value'],
data () {
return {
edit: false,
valueLocal: this.value,
oldValue: (' ' + this.value).slice(1)
}
},
methods: {
save(event){
if(event.target.value){
this.valueLocal = event.target.value;
this.edit = false;
this.$emit('input', this.valueLocal);
}
},
esc(event){
this.valueLocal = this.oldValue;
event.target.value = this.oldValue;
this.edit = false;
this.$emit('input', this.valueLocal);
}
},
watch: {
value: function() {
this.valueLocal = this.value;
}
},
directives: {
focus: {
inserted (el) {
el.focus()
}
}
}
}
</script>

Categories