I wrote this simple questionnaire app example: https://jsfiddle.net/neydmo34/ but I have problem with checkboxes loosing its state when user clicks buttons "Next" and "Back".
For example, if user performs this actions:
answer with "Lisp" on first question,
click "Next",
answer with "Bill Gates" on second question,
click "Back",
click "Next",
then you'll see that the "Bill Gates" checkbox will not be checked anymore, despite the fact that backing array userAnswers is correctly updated.
I cannot understand why that's happen and what I should change in my code to made it work correctly.
Here's the code:
<html>
<head>
<title>Questionnaire</title>
<script src="vue.js"></script>
</head>
<body>
<h1>Questionnaire</h1>
<div id="app">
<p><b>Question {{ currQuestionIndex + 1 }})</b> {{ currQuestion.text }}</p>
<div v-for="ans in currQuestion.answers">
<input type="radio"
:name="currQuestionIndex"
:value="ans"
v-model="userAnswers[currQuestionIndex]" />
<label :for="ans">{{ ans }}</label><br>
</div>
<p>
<button #click="goBack">Back</button>
<button #click="goNext">Next</button>
</p>
userAnswers = {{ userAnswers }}
</div>
</body>
<script>
var app = new Vue({
el: '#app',
data: {
currQuestionIndex: 0,
questions: [
{text: "What's the name of most powerful programming language?",
answers: ['Java', 'C#', 'Lisp', 'Haskell']
},
{text: 'Who is Microsoft founder?',
answers: ['Bill Gates', 'Richard Stallman', 'Steve Jobs']
},
{text: 'What type of software do you like most?',
answers: ['open source', 'closed source', 'public domain']
},
{text: 'The best computing company is:',
answers: ['IBM', 'Microsoft', 'Google']
},
],
userAnswers: [null, null, null, null]
},
computed: {
currQuestion: function () {
return this.questions[this.currQuestionIndex];
}
},
methods: {
goNext: function(e) {
var next = this.currQuestionIndex + 1;
if (next >= this.questions.length) {
alert("Ok, your answers are: " + this.userAnswers);
} else {
this.currQuestionIndex = next;
}
},
goBack: function(e) {
var previous = this.currQuestionIndex - 1;
if (previous >= 0) {
this.currQuestionIndex = previous;
}
}
}
});
</script>
</html>
var app = new Vue({
el: '#app',
data: {
currQuestionIndex: 0,
questions: [
{text: "What's the most powerful programming language?",
answers: ['Java', 'Scheme', 'Lisp', 'Haskell']
},
{text: 'Who is Microsoft founder?',
answers: ['Bill Gates', 'Richard Stallman', 'Steve Jobs']
},
{text: 'What type of software do you like most?',
answers: ['open source', 'closed source', 'public domain']
},
{text: 'The best computing company is:',
answers: ['IBM', 'Microsoft', 'Google']
},
],
userAnswers: [null, null, null, null]
},
computed: {
currQuestion: function () {
return this.questions[this.currQuestionIndex];
}
},
methods: {
goNext: function(e) {
var next = this.currQuestionIndex + 1;
if (next >= this.questions.length) {
alert("OK, your answers are: " + this.userAnswers);
} else {
this.currQuestionIndex = next;
}
},
goBack: function(e) {
var previous = this.currQuestionIndex - 1;
if (previous >= 0) {
this.currQuestionIndex = previous;
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.1/vue.js"></script>
<h1>Questionnaire</h1>
<div id="app">
<p><b>Question {{ currQuestionIndex + 1 }})</b> {{ currQuestion.text }}</p>
<div v-for="ans in currQuestion.answers" :key="ans">
<input type="radio" :name="currQuestionIndex" :value="ans" v-model="userAnswers[currQuestionIndex]" />
<label :for="ans">{{ ans }}</label><br>
</div>
<p>
<button #click="goBack">Back</button>
<button #click="goNext">Next</button>
</p>
userAnswers = {{ userAnswers }}
</div>
You need a key.
When Vue is updating a list of elements rendered with v-for, it by
default uses an “in-place patch” strategy. If the order of the data
items has changed, instead of moving the DOM elements to match the
order of the items, Vue will simply patch each element in-place and
make sure it reflects what should be rendered at that particular
index. This is similar to the behavior of track-by="$index" in Vue
1.x.
This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM
state (e.g. form input values).
To give Vue a hint so that it can track each node’s identity, and thus
reuse and reorder existing elements, you need to provide a unique key
attribute for each item.
Since each answer is unique in your form, you can use :key="ans".
Update: added snippet copied in from Cristy's fiddle.
Related
Hi I was trying to use a v-model an input to a value in object in an array of object in Vue 3. The complexity lies in the fact the object is first processed by a function. And that it need to be processed every time when a change is made to an input. Here is my code (and a sandbox link) :
<template>
<div id="app">
<div v-for="param in process(parameters)" :key="param">
Name : {{param.name}} Value : <input v-model="param.value">
</div>
{{parameters}}
</div>
</template>
<script>
export default {
name: "App",
data(){
return{
parameters :[
{'name':'Richard Stallman','value':'cool dude'},
{'name':'Linus Torvalds','value':'very cool dude'},
{'name':'Dennis Ritchie','value':'very very cool dude'}
]
}
},
methods:{
process(parameters){
const results = parameters.map( param =>{
return {name:param.name+' extra text',
value:param.value+' extra text',
}
})
return results
}
}
};
</script>
I just want the original parameters to change when something is types in the inputs. Maybe #change could be of use. But I didn't find a fix with #change. Does anyone know a solution to my problem? Thanks in advance.
Use computed property to get reactive state of the data.
Working Demo :
new Vue({
el: '#app',
data: {
parameters :[
{'name':'Richard Stallman','value':'cool dude'},
{'name':'Linus Torvalds','value':'very cool dude'},
{'name':'Dennis Ritchie','value':'very very cool dude'}
]
},
computed: {
process() {
const results = this.parameters.map((param) => {
return {
name: param.name + ' extra text',
value: param.value + ' extra text'
}
});
return results;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="param in process" :key="param">
Name : {{param.name}}
Value : <input v-model="param.value">
</div><br>
<strong>Orinigal Data :</strong> {{parameters}}
</div>
I am not entirely sure I understood whether the person should be able to see/edit the text you added within you processing method.
Anyway, I think this sample of code should solve you problem :
<template>
<div id="app">
<div v-for="param in parameters" :key="param.name">
Name : {{ param.name }} Value : <input v-model="param.value" />
</div>
{{ process }}
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
parameters: [
{ name: "Richard Stallman", value: "cool dude" },
{ name: "Linus Torvalds", value: "very cool dude" },
{ name: "Dennis Ritchie", value: "very very cool dude" },
],
};
},
computed: {
process: function() {
const results = this.parameters.map((param) => {
return {
name: param.name + " extra text",
value: param.value + " extra text",
};
});
return results;
},
},
};
</script>
So, we're iterating through the parameters array directly, adding an input on the value just like you did.
When you type in the input, you update the parameter linked to it, in live.
I just switched the method you made into a computed method.
This way, every time parameters is updated, "process" is also updated because it's depending on it directly.
I also removed passing the "parameters" argument, it's in the component data, you can just access it directly.
This way, using "process" just like any variable, you'll always have the updated parameters + what you added to em.
I apologize for the title being a little hard to understand. I had a hard time explaining it in one line. But here's what I'm trying to do.
I'm developing a screen within my app that supports a barcode gun reader. Barcode guns can only interact with textfields. And then through a text field(hidden) I can pass a custom barcode that instructs the UI to do something. Here is the UI explanation for clarity:
I have a radio button group with 2 options (yes and no)
I have a hidden textfield to accept the barcode gun read
I have a barcode for "yes" and another for "no"
If I scan the "yes" barcode, the radio button option with value = "Yes", should be checked
If I scan the "no" barcode, the radio button option with value = "No", should be checked
I initially thought that by changing the v-model to the correct value, it will do it, but it didn't check it. Likewise, by changing the v-model.value to true or false it will check to its appropriate value. But no cigar.
My idea on how this would work is by (pseudocode)
if querySelector with name ragrouphidden.value = "Yes" then find the option whose value is Yes and option.checked = true
else if querySelector with name ragrouphidden.value = "No" then find the option whose value is No and option.checked = true
The "find" part is what eludes me, or maybe there is an easier way.
Here's some relevant code
Template
<div>
<q-input
class="hidden-field"
v-model="ragrouphidden"
name="ragrouphidden"
#change="raSelectOption()">
</q-input>
<div>
<label class="col-6 text-weight-medium">Mark for Remote Adjudication</label>
<div>
<q-option-group
v-model="ragroup"
:options="raoptions"
#check="raRules($event.target.value)"/>
</div>
</div>
</div>
Script
data() {
return {
ragrouphidden: "",
ragroup: null,
raoptions: [
{
label: "Yes",
value: true
},
{
label: "No",
value: false
}
],
}
},
methods: {
raSelectOption() {
setTimeout(() => {
let hasFocus = document.querySelector("input[name=ragrouphidden]");
hasFocus.focus();
}, 500);
if (
document.querySelector("input[name=ragrouphidden]").value === "*yes*"
) {
this.ragroup.value = true; //this is what I need
} else if (
document.querySelector("input[name=ragrouphidden]").value === "*no*"
) {
this.ragroup.value = false; //This as well
}
},
}
Hopefully it makes sense to you guys. Thanks in advance.
You don't need to use ragroup.value to set the model value here. You can simply do this.ragroup = true; and vue will automatically set the q-option-group selected value for you behind the scene.
A simple demo with dynamic checkbox:
var demo = new Vue({
el: '#demo',
data: {
checked: [],
categories: [{ Id: 1 }, { Id: 2 }]
},
mounted(){ this.checked = [2] }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="demo">
<ul>
<li v-for="c in categories">
<input type="checkbox" :value="c.Id" :id="c.Id" v-model="checked" />
{{c.Id}}
</li>
</ul>
</div>
I have created a fiddle to explain what I want : https://jsfiddle.net/silentway/aro5kq7u/3/
The standalone code is as follows :
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="mainApp" class="container-fluid">
<p>This is my main list.</p>
<div class="main-list" v-for="(q, index) in questions">
<input type="checkbox"
v-bind:id="'question-' + index"
v-bind:value="{id: index, property: false}"
v-model="answers">
<label v-bind:for="q">{{q}}</label>
</div>
<p>And this is the list of the selected elements in the previous list.</p>
<ul class="selected-list" v-for="(a, index) in answers" :key="a.id">
<li>{{questions[a.id]}} <input type="checkbox"
v-bind:id="'answer-' + index"
v-bind:value="true"
v-model="a.property">
</li>
</ul>
<p>Here's the answer's array for debugging: {{answers}}</p>
</div>
<script>
var mainApp = new Vue({
el: '#mainApp',
data: {
questions: [
"Who are you ?",
"Who, who?",
"You know my name?",
"Look up my number"
],
answers: []
}
});
</script>
I want to display a first list of questions, each with a checkbox. The selected questions are stored in an array called "answers".
From these selected answers I then make another list. Each item has a new corresponding checkbox, for a certain property (which can be true or false). I would like this associated property to be stored in the same array ("answers") as the results from the input in the first list.
What happens with my code is that checking a box in the second list does change the shared array of data ("answers"), but in doing so it also unchecks the corresponding answer in the first list.
Any help would be much appreciated.
I'm having a very hard time following your wording but I gave it a shot anyway. I think you'd be better off keeping selected questions and selected answers in their own array and use a computed property to join them basically. Here's a quick fiddle of it: https://jsfiddle.net/crswll/d8e1g750/21/
new Vue({
data: {
questions: [{
id: 1,
question: 'What the heck 1?'
},
{
id: 2,
question: 'What the heck 2?'
},
{
id: 3,
question: 'What the heck 3?'
},
{
id: 4,
question: 'What the heck 4?'
},
{
id: 5,
question: 'What the heck 5?'
},
],
selectedQuestions: [],
selectedAnswers: [],
},
computed: {
answers() {
return this.selectedQuestions.map(id =>
this.questions.find(question => question.id === id)
)
},
selectedAnswersSimpleList() {
return this.selectedAnswers
.map(id => this.questions.find(question => question.id === id))
.map(question => question.question)
}
},
}).$mount('#app')
Vue JS computed property is not triggered With this markup
<!-- language: lang-html -->
<p>£{{plant_price}}</p>
<div v-if="selected.plant.variations.length > 0 ">
<select v-model="selected.plant.selected_variation" class="form-control">
<!-- inline object literal -->
<option v-for="(variation, i) in selected.plant.variations" :selected="variation.id == selected.plant.selected_variation ? 'selected' : ''":value="variation.id">
{{variation.name}}
</option>
</select>
</div>
<!-- language: lang-js -->
var app = new Vue({
el: '#vueApp',
data: {
selected: {
type: {a: '' , b: ''},
vehicle: '',
plant: {
}
},
computed: {
plant_price: function() {
if (this.selected.plant.variations.length > 0 ) {
var variant = _.find(this.selected.plant.variations, {id: this.selected.plant.selected_variation });
return variant.price;
} else {
return this.selected.plant.price;
}
}
...
selected.plant is populated by clicking on a plant - triggering the updateSelected method.
<div class="col-sm-4" v-for="(plant, i) in step2.plants">
<div v-on:click="updateSelected(plant)" ....
methods: {
updateSelected: function(plant) {
this.selected.plant = plant; // selected plant
if (this.selected.plant.variations.length > 0 ) {
this.selected.plant.selected_variation = this.selected.plant.variations[0].id; // set the selected ID to the 1st variation
I have checked through the debugger, and can see that all the correct properties are available.
selected:Object
type:Object
vehicle: "Truck"
plant:Object
id:26
price:"52"
regular_price:"100"
selected_variation:421
variations:Array[2]
0:Object
id:420
name:"small"
price:52000
regular_price:52000
1:Object
etc...
I have a computed property, which should update the plant_price based on the value of selected.plant.selected_variation.
I grab selected.plant.selected_variation and search through the variations to retrieve the price. If no variation exists, then the plant price is given.
I have a method on each product to update the selected plant. Clicking the product populates the selected.plant and triggers the computed plant_price to update the price (as the value of selected.plant.selected_variation has changed).
However, the computed plant_price is not triggered by the select. Selecting a new variant does what its supposed to, it updates selected.plant.selected_variation. Yet my plant_price doesn't seem to be triggered by it.
So I refactored my code by un-nesting selected.plant.selected_variation. I now hang it off the data object as
data = {
selected_variation: ''
}
and alter my computer property to reference the data as this.selected_variation. My computed property now works??? This makes no sense to me?
selected.plant.selected_variation isn't reactive and VM doesn't see any changes you make to it, because you set it after the VM is already created.
You can make it reactive with Vue.set()
When your AJAX is finished, call
Vue.set(selected, 'plant', {Plant Object})
There're two ways you can do it, what you are dealing with is a nested object, so if you want to notify the changes of selected to the others you have to use
this.$set(this.selected, 'plant', 'AJAX_RESULT')
In the snippet I used a setTimeout in the created method to simulate the Ajax call.
Another way you can do it is instead of making plant_price as a computed property, you can watch the changes of the nested properties
of selected in the watcher, and then update plant_price in the handler, you can check out plant_price_from_watch in the snippet.
Vue.component('v-select', VueSelect.VueSelect);
const app = new Vue({
el: '#app',
data: {
plant_price_from_watch: 'not available',
selected: {
type: {a: '' , b: ''},
vehicle: "Truck"
}
},
computed: {
plant_price() {
return this.setPlantPrice();
}
},
watch: {
selected: {
handler() {
console.log('changed');
this.plant_price_from_watch = this.setPlantPrice();
},
deep: true
}
},
created() {
setTimeout(() => {
this.$set(this.selected, 'plant', {
id: 26,
price: '52',
regular_price: '100',
selected_variation: 421,
variations: [
{
id: 420,
name: "small",
price: 52000,
regular_price: 52000
},
{
id: 421,
name: "smallvvsvsfv",
price: 22000,
regular_price: 22000
}
]
})
}, 3000);
},
methods: {
setPlantPrice() {
if (!this.selected.plant) {
return 'not available'
}
if (this.selected.plant.variations.length > 0 ) {
const variant = _.find(this.selected.plant.variations, {id: this.selected.plant.selected_variation });
return variant.price;
} else {
return this.selected.plant.price;
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.js"></script>
<div id="app">
<p>£{{plant_price}}</p>
<p>£{{plant_price_from_watch}}</p>
<div v-if="selected.plant && selected.plant.variations.length > 0 ">
<select v-model="selected.plant.selected_variation" class="form-control">
<!-- inline object literal -->
<option v-for="(variation, i) in selected.plant.variations" :selected="variation.id == selected.plant.selected_variation ? 'selected' : ''":value="variation.id">
{{variation.name}}
</option>
</select>
</div>
</div>
A few days ago I started using vue.js and trying to get the hang of it.
I've been fiddling quite a bit to get this easy example to work: reading the value of selected checkboxes in components with vue.js .
Please see my example on http://jsbin.com/gukoqo/edit?html,js,output
How can I let selected in the parent instance contain the selected values of the checkbox? E.g., filter_a and filter_c are selected, then selected should contain an array: ['filter_a', 'filter_c']
I expected vue.js to make this very easy, but don't know yet how to. Anyone? :)
I'm using the latest vue.js (2.3.3 at the moment)
One possible way.
Vue.component('facet-filter', {
props: ['filter', 'checked'],
template: `<div>
<label class="form-check-label">
<input #change="$emit('change', filter.text, $event)"
class="form-check-input"
type="checkbox"
:value="filter.text"
:checked="checked"
name="filters"> {{filter.text}}
{{$props | json 2}}</label>
</div>`,
});
new Vue({
el: '#app',
data: {
filterFacets: [
{ id: 0, text: 'filter_a' },
{ id: 1, text: 'filter_b' },
{ id: 2, text: 'filter_c' },
{ id: 3, text: 'filter_d' },
],
selected: [], // How can I let this contain ['filter_a', 'filter_b'] etc. when selected?
},
methods:{
onChange(filter, $event){
if ($event.target.checked)
this.selected.push(filter)
else {
const index = this.selected.findIndex(f => f === filter)
if (index >= 0)
this.selected.splice(index, 1)
}
}
}
});
And change your template to
<div id="app">
<facet-filter
v-for="item in filterFacets"
v-bind:filter="item"
v-bind:checked="selected.includes(item.text)"
:key="item.id"
#change="onChange"
>
</facet-filter>
<p><pre>data: {{$data | json 2}}</pre></p>
</div>
Updated bin.