I'm looking into making a quiz where the user will be greeted with a "card" interface with both simple yes/no questions and some multiple choice questions. When answering one question, the next questions change to fit the previous answers.
Example flow:
Do you want to eat out or at home? I want to eat out. Do you want to eat at a Korean restaurant? Yes.
The problem I have is that I want this on the frontend without having multiple routes. Currently, I'm using Vue.js and Vue-Router. And this is what I have so for(don't mind the naming conventions, just temporary):
<template>
<div>
<div v-show="isQuestion(0)">
<p id="question_1">Question 1</p>
<p #click="answerQuestion()">Answer 1</p>
<a #click="answerQuestion()">Answer 2</a>
</div>
<div v-show="isQuestion(1)">
<p id="question_2">Question 2</p>
<div v-show="isAnswer(0)">
<p>Game?</p>
<a #click="answerQuestion()">Yes</a>
<a #click="nextAnswer()">No</a>
</div>
<div v-show="isAnswer(1)">
<p>Read?</p>
<a #click="answerQuestion()">Yes</a>
<a #click="nextAnswer()">No</a>
</div>
<div v-show="isAnswer(2)">
<p>Redo?</p>
<a #click="resetAnswer()">Reset</a>
</div>
<a #click="search()">Search</a>
</div>
</div>
</template>
<script>
export default {
data () {
return {
question: 0,
answer: 0,
answers: {}
}
},
methods: {
answerQuestion () {
this.answers[this.question] = this.answer
this.question++
this.answer = 0
},
nextAnswer () {
this.answer++
},
resetAnswer () {
this.answer = 0
},
isQuestion (n) {
return n === this.question
},
isAnswer (n) {
return n === this.answer
}
}
}
</script>
One option I'm thinking about would perhaps to put the questions with the answers in a database so that the frontend can fetch them as JSON and then populate the so called "cards". But then I have a problem with how to show the "correct" next question responding to the previous answers.
I don't feel comfortable hard coding everything as it seems like a bad practice but I'm having a hard time doing this any other way.
I think the point on your case is proper data structure. In your case I will use:
data () {
return {
curentQuesionIndex: 0,
curentQuesion: null,
questions: [
{
question: 'Do you want to eat out or at home?',
options: [{0:'No'}, {1:'Yes'}, {'special':'reset'}], // answer options
answer: 1 // if NULL then user not yet give answer
// if 'special' then you process it differently
},
... // next questions here
]
}
}
Using this array of questions you can render it in automatic way using Vue, (you can read it from ajax json), show next questions and other stuff. If answer in some question is NULL then you know that this is the 'next question' to show...
In your vue component create variable curentQuesionIndex=2 and currentQuestion= {..} which you will use to show, save (into your query array), and operate (for instance of 'special' answer like 'reset').
You will use only one #click function: saveAnswer(value) where 'value' is the one option from question.options. Inside this funciotn you can save answer to questions list, set newq question to currentQuestion variable (which is rendered on screen) and depends of value you will make different actions - for instance: you will put if statement there: if questions[currentQuestionIndex].options[value] == 'reset' then you will reset...
Related
I'm new to Vue.js (with a background in Computer Science and programming, including interactive Javascript webpages) and as I'm a teacher, I have a quiz site I use to give homework to my students.
My codebase is messy, so I decided to migrate the whole thing to Vue, with the idea that I could use a component for each individual type of question -- separation of concerns, and all that.
However, I can't seem to find a way to generate appropriate components on the fly and include them in my page.
Here's a simplified version of my framework, with two question types. If I include the components directly in the HTML, they work fine.
Vue.component("Freetext",{
props: ["prompt","solution"],
data : function() {return {
response:""
}},
methods : {
check : function () {
if (this.solution == this.response) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'<span><h1>{{prompt}}</h1> <p><input type="text" v-model="response"></input></p> <p><button class="LG_checkbutton" #click="check()">Check</button></p></span>'
})
Vue.component("multi",{
props : { prompt: String,
options : Array,
key_index : Number // index of correct answer
},
data : function() {return {
response:""
}},
methods : {
check : function (k) {
if (k == this.key_index) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'<span><h1>{{prompt}}</h1><button v-for="(v,k) in options" #click="check(k)">{{v}}</button></span>'
})
</script>
<div id="app">
<Freetext prompt="Type 'correct'." solution="correct"></freetext>
<multi prompt="Click the right answer." :options='["right","wrong","very wrong"]' :key_index=0></multi>
</div>
<script>
var app = new Vue({
el: "#app",
data : {
questions:[ {type:"Multi",
prompt: "Click the right answer.",
options:["right","wrong","very wrong"],
key:0},
{type:"Freetext",
prompt:"Type 'correct'.",
solution:"correct"}
],
question_number:0
},
methods : {
nextQuestion : function () {
this.question_number ++;
}
}
})
</script>
But what I want to do is generate the contents of the div app on the fly, based on using the data member app.question_number as an index to app.questions, and the .type member of the question indicated (i.e. app.questions[app.question_number].type)
If I try to make the app of the form:
{{question}}
</div>
<script>
//...
computed : {
question : function () {
var typ = this.questions[this.question_number].type;
return "<"+typ+"></"+typ+">";
}
...I just get as plain text, and it isn't parsed as HTML.
If I try document.getElementById("app").innerHTML = "<multi prompt='sdf'></multi>"; from the console, the tag shows up in the DOM inspector, and isn't processed by Vue, even if I call app.$forceUpdate().
Is there any way round this?
While Keith's answer works for most of what I need to do, there's another way to handle this that I've just found out about, which I thought I'd share in case anyone else is looking for it: giving a block level HTML element a v-html property.
For me, this is handy as a short term fix as I'm migrating a codebase that generates dynamic HTML as strings, and I can quickly integrate some of my existing code without reworking it completely.
For example, I have a function makeTimetable that takes a custom datastructure representing a week's actively and turns it into a table with days across the top and times down the left-hand side, setting appropriate rowspans for all the activities. (It's a bit of a convoluted function, but it does what I need and isn't really worth refactoring at this point.)
So I can use this as follows:
<script type="text/x-template" id="freetext-template">
<span>
<div v-html="tabulated_timetable"></div>
<p>{{prompt}}</p>
<p><input type="text" v-model="response"></input></p>
<p><button class="LG_checkbutton" #click="check()">Check</button></p>
</span>
</script>
<script>
var freetext = Vue.component("Freetext",{
props: {"prompt":String,
"timetable":Object,
"solution":String,
data : function() {return {
response:""
}},
computed : {
tabulated_timetable : function () {
return makeTimetable (this.timetable);
}},
methods : {
check : function () {
if (this.solution == this.response) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'#freetext-template'
})
</script>
(I suppose I could put `tabulated_timetable` in `methods` rather than `computed`, as it's set once and never changed, but I don't know if there would be any performance benefit to doing it that way.)
I think maybe a slightly different approach, Vue supports the concept of "dynamic components"
see https://v2.vuejs.org/v2/guide/components-dynamic-async.html
this will let you define what component to use on each question which would look something like
<component v-bind:is="question.component" :question="question"></component>
I have tried googling and searching entirety of stack overflow for this question but I think it boils down to the keywords I'm using to search.
Basically my problem boils down to the following: when the cursor leaves an element, wait 500 milliseconds before closing the element. Before close the element, check if the cursor is back in the element, and if its not, do not hide it.
I'm using vuejs to do this but I boiled down the problem to being in setTimeout function. The part where I have the code is fairly complex to post it here, therefore I created a simple POC to demonstrate the problem:
<template>
<div id="app">
<ul v-for="x in 2000" :key="x">
<li #mouseenter="handleMouseEnter(x)" #mouseleave="handleMouseLeave(x)" style="height: 50px;">
Hello
<span style="background-color: red" v-show="showBox[x]">BOX</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "App",
components: {},
methods: {
handleMouseEnter(index) {
setTimeout(() => {
let showBox = [...this.showBox];
showBox[index] = true;
this.showBox = showBox;
}, 500);
},
handleMouseLeave(index) {
let showBox = [...this.showBox];
showBox[index] = false;
this.showBox = showBox;
}
},
data() {
return {
showBox: []
};
},
created() {
for (let i = 0; i <= 2000; i++) {
this.showBox[i] = false;
}
}
};
</script>
You can checkout the sandbox here: https://codesandbox.io/s/cold-river-ruz7b
If you hover over from top to bottom in a moderate speed you will realize that even after leaving the li element the red box stays.
I guess the problem lays in the fact that handleMouseEnter is being called with a setTimeout and the handleMouseLeave is not. Therefore, making handleMouseEnter be executed after handleMouseLeave therefore showing the box.
Any light would be highly appreciated here and if a short explanation could be given on why the problem is happening it would be great
Your example seems to operate the opposite way round to the original problem description (the timer is on showing not hiding) but I think I get what you mean.
As you suggest, the problem is that the timer callback is being called after the mouseleave event fires. So the red box does get hidden but shortly thereafter the timer fires and brings it back.
In the example below I have simply cancelled the timer using clearTimeout. In general it might be necessary to store an array of such timers, one for each element, but in this specific example I believe it only makes sense to have one timer active at once so I can get away without an array.
I also moved the initial population of showBox into data. There seemed no reason to use a created hook here.
There's no need to copy the whole array each time, you can just use $set to set the value in the existing array. I haven't changed that in my example.
I would also note that for this particular example you don't need an array to hold all the showBox values. Only one red box can be visible at once so you only need a single property to hold the index of the currently visible box. I haven't changed this in my example as I suspect your real use case is not as straight forward as this.
new Vue({
el: '#app',
methods: {
handleMouseEnter(index) {
this.currentTimer = setTimeout(() => {
let showBox = [...this.showBox];
showBox[index] = true;
this.showBox = showBox;
}, 500);
},
handleMouseLeave(index) {
clearTimeout(this.currentTimer)
let showBox = [...this.showBox];
showBox[index] = false;
this.showBox = showBox;
}
},
data() {
const showBox = [];
for (let i = 0; i <= 2000; i++) {
showBox[i] = false;
}
return {
showBox
};
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<ul v-for="x in 2000" :key="x">
<li #mouseenter="handleMouseEnter(x)" #mouseleave="handleMouseLeave(x)" style="height: 50px;">
Hello
<span style="background-color: red" v-show="showBox[x]">BOX</span>
</li>
</ul>
</div>
I am re-vamping a program for my Engineering departments research team, my app has been built using Electron, Vuex, HTML/Sass/Javascript. The program's purpose is to allow the user to create "steps" which are items/tiles that you can go into & set system parameters & processes to run X amount of times. Setting up the values and settings within each step can take a while, so if the user creates 5 steps, and realizes they want to delete step number 2 entirely, there needs to be a 'remove' or a small 'x' within each step/item. Currently, I can only remove the last populated step/item using the 'pop' function to remove the last item in the array 'stepsGrid'.
Using vue-grid-layout to populate grid-items/tiles which will each be a 'step' that our system will run. I am able to dynamically add new 'steps', however I have only been able to successfully remove steps by using pop() to remove the last item from the array in which grid-items are. I want to place a
<button id="remove" #click="removeStep()">Remove</button>
within each step, so that I can delete any item on the grid instead of just the last item.
I have seen a few examples, but not being the best at javascript- I haven't had any luck. I tried referencing the functions used here: https://chrishamm.io/grid-demo/
and https://github.com/chrishamm/vue-dynamic-grid
for removing grid items, however this library is a little more complicated than the regular Vue-Grid-Layout.
Here is my code, the key is the step.i component which is the ID of each item populated, each tile will have an 'x' in the corner and the function needs to be able to recognize the ID of that tile, and delete the corresponding item from the stepsGrid array:
<h2 style="color: #f6a821;">Steps</h2>
<hr class="hr" />
<grid-layout
:layout.sync="stepsGrid"
:col-num="8"
:row-height="75"
:is-draggable="true"
:is-resizable="false"
:is-mirrored="false"
:vertical-compact="true"
:margin="[50, 50]"
:use-css-transforms="true">
<grid-item
v-for="step in stepsGrid" :key= "step.i"
:x="step.x"
:y="step.y"
:w="step.w"
:h="step.h"
:i="step.i"
:isDraggable="step.isDraggable">
<div class="Panel__name">Step: {{step.i}} //displays item # on each tile
<div class="Panel__stepcount"> Loop Count: <input type="number" value="1">
</div>
</div>
<div class="editButton">
<div class="Panel__status">Status:</div>
<button #click="removeStep()">Remove</button>
</grid-item>
</grid-layout>
//Grid arrays are in store.js, as shown below(this is for an electron app):
import Vue from 'vue';
import Vuex from 'vuex';
import createPersistedState from 'vuex-persistedstate'
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
stepsGrid : [
{"x":0,"y":0,"w":2,"h":1,"i":"0"}
],
mutations: {
addStep (state, step){
state.stepsGrid.push(step);
}
},
actions: {
addStep ({state, commit}){
const step =
{"x":0, "y": 1, "w": 2,"h":1,"i":String(state.stepsGrid.length) };
commit('addStep', step);
},
I know I need to incorporate the use of a key here, but I am unsure how to do it. I found this, but he goes about it an unusual way:
"I deleted the element in array by the button and could not understand the problem, as the last element was always deleted, although the key was unique. I added an extra unique uuid and it all worked."
<dish-item v-else class="dishItem"
v-for="(item, key) in dishes" :key="key + $uuid.v1()" :value="item"
#removeDish="$delete(dishes, key)"
#editDish="$refs.modal.showEdit({item, key})"
/>
Lastly, I went digging through the files for Vue-Dynamic-Grid and found that they are removing items with the following method:
methods: {
removeElement(item) {
if (item == this.selectedItem) {
this.selectElement(null);
}
this.gridItems.splice(item.i, 1);
this.items.splice(item.i, 1);
breakpoints.forEach(function(size) {
this.layouts[size].splice(item.i, 1);
this.layouts[size].forEach(function(layout, i) {
if (layout.i > item.i) {
layout.i--;
}
});
}, this);
this.$emit("itemRemoved", item);
}
If anyone could be of help, I would appreciate it! I have been learning JS on the go with this project, so if anyone has some tips please let me know! Thanks
I have tried attempting to write a function based on the way Vue-Dynamic-Layout's method for removing items, but I was not successful. As of right now, I don't have a small 'x' in each tile, I have a button for adding/removing a step in the bottom left of my app, to remove items I am simply popping the last item out of the stepsGrid array, so I can only remove the last populated item.
First let the event handler know which step is being clicked
<button #click="removeStep(step)">Remove</button>
Then find that step in the array and remove it
methods: {
removeStep(step) {
const index = this.stepsGrid.indexOf(step);
if (index >= 0) this.stepsGrid.splice(index, 1);
}
}
I am building out a new app with Vue.js and have come across what I thought would be a simple problem but can not find a solution yet.
I have a question with three answers. I only want the user to be able to click an answer once which I know I can use
#click.once but I need to let the user know it is no longer clickable in the UI. I have tried some like <button :disabled="submitted" #click="checkAnswer('q1','3'), submitted = true"></button> but this disables all three buttons at the same time. Can someone please show me how to disable a button once the user has clicked it and keep it independent from other buttons? I really appreciate any help.
HTML:
<ul>
<li><button class="btn blue" #click="checkAnswer('q1','1')">Answer 1 for q1</button></li>
<li><button class="btn blue" #click="checkAnswer('q1','2')">Answer 2 for q1</button></li>
<li><button class="btn blue" #click="checkAnswer('q1','3')">Answer 2 for q1</button></li>
</ul>
Code:
export default {
name: 'Questions',
props: ['clue'],
data(){
return {
feedback: null,
answer: null,
answers: {
q1: '2',
q2: '1',
q3: '3'
}
}
},
methods: {
checkAnswer(q, a) {
if(a == this.answers[q]){
this.answer = "Congrats!"
} else {
this.answer = "Sorry that was the wrong answer."
}
}
}
}
Pass $event as the third argument, then in the handler, event.target.disabled = true
This is really the quick and dirty approach, which is ok as long as that's the end of the road for the buttons. A more robust approach would be to make a component to use for each of the buttons, and have a state variable for whether it is disabled.
I have a problem. It is:
let list = storage.map((element, index, array) => {
return (
<li key={index} className="list-element">
<div className="title-wrapper" onMouseEnter={this.handleMouseEnter}>
<p className="title">{array[index]['title']}</p>
<p className="title title-full" ref={node => this.title = node}>Text</p>
</div>
</li>
);
});
handleMouseEnter() {
this.title.style.opacity = "1";
}
So, when mouse enters .title-wrapper I want to set opacity to 1 on .title-full. But no matter on which .title-wrapper mouse enters, always opacity will be set to the last .title-full.
The problem is easy to solve with querySelector but I read that using it is bad thing in React, isn't it?
The reason this.title is always set to the last element is because you are setting each element in the loop to this.title, so the last one overwrites the one before it, and so on.
What about just using CSS directly, instead of handling it in React at all?
Example:
.title-wrapper:hover .title-full {
opacity: 1;
}
Just a general comment that refs aren't usually preferred in React (maybe for forms or modals sometimes). What you're emulating is a jQuery-like DOM manipulation approach, which can certainly work but is sidestepping the power of React being state-based, obvious, and easy to follow.
I would typically
this.setState({
hovered: true
})
in your handleMouseEnter method (and unset it in your mouseOut). Then choose your className based on this.state.hovered
I think going with CSS is definitely the best approach.
Just for anyone running into this issue of multiple refs in another context, you could solve the issue by storing the refs in an array
let list = storage.map((element, index, array) => {
return (
<li key={index} className="list-element">
<div className="title-wrapper" onMouseEnter={() => this.handleMouseEnter(index)}>
<p className="title">{array[index]['title']}</p>
<p className="title title-full" ref={node => this.titles[index] = node}>Text</p>
</div>
</li>
);
});
handleMouseEnter(index) {
this.titles[index].style.opacity = "1";
}
Again, you don't need to do this for your use case, just thought it might be helpful for others :D