I have a list of checkboxes that are rendered with a v-for on a component. When checked, the checkboxes fill up an array with currently selected checkboxes. The problem comes when one or more of the checked checkboxes is removed - the v-model still counts the removed checkbox. How do I update the v-model as my array updates? I tried force re-rendering the whole component which solves the problem but it's not the solution I need.
<div v-for="player in players" :key="player.id">
<input
v-model="selectedPlayers"
:value="player.id"
type="checkbox"
:id="player.id"
/>
<label :for="player.id">
{{ player.name }}
</label>
</div>
Sandbox
Problem
Desired outcome
V-model won't remove data when the component is no longer rendered, you need to do that explicitly.
You could filter selectedPlayers from the #click handler so that it only includes ids that are in the new variable.
this.selectedPlayers = this.selectedPlayers.filter(
id => players2.find(
player => player.id === id
)
)
So from what I understand, you have this player2 array, that you need to compare against. Whatever is there in the players2 array needs to be there in the selectedPlayers array. To do this just use the use map array function to iterate over the players2 array to return only the ids of the players and then store them in the selected players array which being a reactive property will automatically patch the DOM. There's absolutely no need to re-render the component.
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
el: "#app",
data() {
return {
selectedPlayers: [],
players: [
{name: "Arnold",id: "1"},
{name: "Rambo",id: 2},
{name: "Terminator",id: 3},
{name: "Titan",id: 4},
{name: "Odin",id: 5},
],
players2: [
{name: "Titan",id: 4},
{name: "Odin",id: 5},
],
};
},
methods: {
clicked() {
this.players = this.players2;
this.selectedPlayers = this.players2.map(p => p.id);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
If you check all checkboxes and press the button, the array still contains all elements
<div v-for="player in players" :key="player.id">
<input v-model="selectedPlayers" :key="players.length" :value="player.id" type="checkbox" :id="player.id" />
<label :for="player.id">
{{ player.name }}
</label>
</div>
{{ selectedPlayers }}
<button #click="clicked">Press me</button>
</div>
Related
Desperately in need of your help guys.
So basically I have a custom checkbox component whit a v-model. I use a v-for loop on the component to display checkboxes with the names from the array. In the parent component I have two columns Available and Selected. The idea is that if I check one of the boxes in the Available column it should appear on the Selected column. The problem is that it displays letter by letter and not the full name.
I am able to achieve the desired result without having a checkbox component, but since I will be needing checkboxes a lot throught my project I want to have a component for it.
Please follow the link for the code:
CodeSandBox
Dont mind the difference in styling.
The problem:
The desired outcome:
There are two problems. The first problem is, that you have your v-model set to v-model="filter.filterCollection", so a checkbox you select will be stored into the array as a string and if you select another checkbox the string gets overwritten. The second problem is, that you call that stored string as an array. That causes, that your string, which is an array of letters, will be rendered for each letter. So 'Number' is like ["N", "u", "m", "b", "e", "r"].
To solve your problem, you need to store every selection with its own reference in your v-model. To cover your needs of correct listing and correct deleting you need to apply the following changes:
Your checkbox loop
<Checkbox
v-for="(item, index) in items"
:key="item.id"
:label="item.name"
:id="index"
:isChecked="isChecked(index)" // this is new
#remove-selected-filter="removeSelectedFilter" // This is new
:modelValue="item.name"
v-model="filter.filterCollection[index]" // Change this
/>
Your v-model
filter: {
filterCollection: {} // Object instead of array
}
Methods in FilterPopUp.vue
methods: {
removeSelectedFilter(index) {
delete this.filter.filterCollection[index];
},
isChecked(index) {
return !!this.filter.filterCollection[index];
}
}
Your Checkbox.vue:
<template>
<label>
<p>{{ label }}</p>
<input
type="checkbox"
:id="id"
:value="modelValue"
:checked="isChecked"
#change="emitUncheck($event.target.checked)"
#input="$emit('update:modelValue', $event.target.value)"
/>
<span class="checkmark"></span>
</label>
</template>
<script>
export default {
name: "Checkbox",
props: {
modelValue: { type: String, default: "" },
isChecked: Boolean,
label: { type: String },
value: { type: Array },
id: { type: Number },
},
methods: {
emitUncheck(event) {
if(!event){
this.$emit('remove-selected-filter', this.id);
}
}
}
};
</script>
This should now display your items properly, delete the items properly and unselect the checkboxes after deleting the items.
StevenSiebert has correctly pointed to your errors.
But his solution is not complete, since the filters will not be removed from the collection when you uncheck one of them.
Here is my complete solution of your checkbox working as expected:
Checkbox.vue
<template>
<label>
<p>{{ label }}</p>
<input
type="checkbox"
:id="id"
v-model="checked"
#change="$emit('change', { id: this.id, checked: this.checked})"
/>
<span class="checkmark"></span>
</label>
</template>
<script>
export default {
name: "Checkbox",
props: {
modelValue: { type: Boolean, default: false },
label: { type: String },
id: { type: Number },
},
emits: ["change"],
data() {
return {
checked: this.modelValue
};
}
};
</script>
FilterPopUp.vue
<template>
...
<Checkbox
v-for="(item, index) in items"
:key="index"
:label="item.name"
:id="index"
#change="onChange"
/>
...
</template>
<script>
...
methods: {
removeSelectedFilter(index) {
this.filter.filterCollection.splice(index, 1);
},
onChange(args) {
const {id, checked} = args;
const item = this.items[id].name;
if (checked) {
if (this.filter.filterCollection.indexOf(item) < 0) {
this.filter.filterCollection.push(item);
}
} else {
this.filter.filterCollection = this.filter.filterCollection.filter( i=> i != item);
}
},
},
...
Here is the working CodeSandbox:
https://codesandbox.io/s/pensive-shadow-ygvzb?file=/src/components/Checkbox.vue
Sure, there are many ways to do it. If somebody has a nicer and shorter way to do it, please post your solution. It will be interesting to look at it.
I have a v-btn whose content is determined by values in the deepest layer of a multi-dimension array.
<div v-for="student in students">
<v-btn disabled v-for="tag in student.tags">{{tag}}</v-btn>
</div>
Here tags is a sub-array.
I want to re-render this button after the values change, but don't know how.
I have already used Vue.set like:
// "this.students" is an array, and "this.students[index].tags" is a sub-array.
// I increment the length of the sub-array.
this.students[index].tags.length++;
// Then add a new value into the sub-array at the end.
Vue.set(this.students[index].tags, this.students[index].tags.length - 1, value)
By printing out to the console, I can see both the values and the length of the sub-array, this.students[index].tags, change, and there should be a new button appear because I added a new value into this.students[index].tags, but there is not. And only after I re-compile the client end, the new button show up.
Could anyone teach how to re-render that button?
Thanks in advance!
Vue only observes the object's own properties - that is, only 1 level deep, no more. So you can try one of these:
use this.$forceUpdate(); (https://v2.vuejs.org/v2/api/#vm-forceUpdate)
use this.$set(this.students[index], 'tags', this.students[index].tags.concat([value])); - once set, the tags array will be observed by Vue so you can use tags.push() on subsequent additions
use a hash-map for students' tags
computed:
{
studentTags()
{
const result = {};
this.students.forEach(student =>
{
result[student.id] = student.tags;
});
return result;
}
},
methods:
{
addTag(studentId, tag)
{
this.studentTags[studentId].push(tag);
}
}
We do not need to use Vue.set to push the new data in an array or sub-array. It will auto-handle by the vuejs.
However, we should use Set to reflect the updates in a sub-array.
See this example-
<template>
<div id="app">
<div v-for="(student, index) in students" :key="index">
<button #click="addMoreTag(student.id)" style="background: green">
Add more
</button>
<button
v-for="(tag, tindex) in student.tags"
:key="tindex + 't'"
#click="updateTag(student.id, tag)"
>
{{ tag }}
</button>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
students: [
{
id: 1,
name: "John",
tags: ["one", "two"],
},
{
id: 2,
name: "Mira",
tags: ["three", "five"],
},
],
};
},
methods: {
addMoreTag(id) {
let index = this.students.findIndex((item) => item.id === id);
this.students[index].tags.push("new");
},
updateTag(student_id, tag) {
let index = this.students.findIndex((item) => item.id === student_id);
let tagIndex = this.students[index].tags.findIndex(
(item) => item === tag
);
this.$set(this.students[index].tags, tagIndex, "updated");
},
},
};
I wrote in coodepen too- https://codesandbox.io/s/blissful-cdn-5kyt1c?file=/src/App.vue
I'm using a computed list to display several forms for changing comments in a database. (backend Symfony / api requests via axios, but unrelated)
The form for the comments itself is in a Vue component.
The computed list is based on a list that gets loaded (and set as data property) when the page is mounted which is then filtered by an input search box in the computed property.
Now when i type different things in the input box and the comment component gets updated the v-model and labels are messing up.
I've tested in several browsers and the behaviour is the same in the major browsers.
I've also searched the docs and haven't found a solution.
Example to reproduce behaviour:
<!DOCTYPE html>
<html>
<div id="app"></app>
</html>
const ChangeCommentForm = {
name: 'ChangeCommentForm',
props: ['comment', 'id'],
data() {
return {
c: this.comment,
disabled: false
};
},
template: `
<form>
<div>{{ comment }}</div>
<input :disabled="disabled" type="text" v-model="c">
<button type="submit" #click.prevent="changeComment">
Change my comment
</button>
</form>
`,
methods: {
changeComment() {
this.disabled = true;
// do the actual api request (should be unrelated)
// await api.changeCommentOfFruit(this.id, this.c),
// replacing this with a timeout for this example
window.setTimeout(() => this.disabled = false, 1000);
}
}
};
const App = {
components: {ChangeCommentForm},
data() {
return {
fruits: [
{id: 1, text: "apple"},
{id: 2, text: "banana"},
{id: 3, text: "peach"},
{id: 4, text: "blueberry"},
{id: 5, text: "blackberry"},
{id: 6, text: "mango"},
{id: 7, text: "watermelon"},
],
search: ''
}
},
computed: {
fruitsFiltered() {
if (!this.search || this.search === "")
return this.fruits;
const r = [];
for (const v of this.fruits)
if (v.text.includes(this.search))
r.push(v);
return r;
}
},
template: `
<div>
<form><input type="search" v-model="search"></form>
<div v-for="s in fruitsFiltered">
<ChangeCommentForm :id="s.id" :comment="s.text"/>
</div>
</div>
`
};
const vue = new Vue({
el: '#app',
components: {App},
template: '<app/>'
});
Just type some letters in the search box
Example on codepen: https://codepen.io/anon/pen/KLLYmq
Now as shown in the example the div in CommentChangeForm gets updated correctly, but the v-model is broken.
I am wondering if i miss something or this is a bug in Vue?
In order to preserve state of DOM elements between renderings, it's important that v-for elements also have a key attribute. This key should remain consistent between renderings.
Here it looks like the following might do the trick:
<div v-for="s in fruitsFiltered" :key="s.id">
<ChangeCommentForm :id="s.id" :comment="s.text"/>
</div>
See:
https://v2.vuejs.org/v2/guide/list.html#Maintaining-State
https://v2.vuejs.org/v2/api/#key
I am having a little problem that it's making headaches.
I have a modal where I show some info with checkboxes, the information, comes from an array, and I set the checkbox states from that array, and example of the array:
this.array = [
{scope: "acc", code: "1", alias: "aaa", selected: true, editable: true},
{scope: "acc", code: "2", alias: "bbb", selected: true, editable: true}
]
The thing that I want to do is to play as normal with the checks, but when i click a discardChanges button, the checkboxes, return the state that they were previously.
<div *ngFor="let account of allAccountsList; let i = index;" class="">
<div class="row">
<input (click)="saveCheck(account.code, account.scope)" [(checked)]="account.selected"
type="checkbox" name="genres" value="adventure" id="{{i}}">
<label for="{{i}}" style="font-family: 'SExtralight'; font-size:14px;"></label>
</div>
</div>
Thank you all.
This code:
<div *ngFor="let account of array1; let i = index;">
<input [checked]="account.selected"
type="checkbox"
name="genres"
id="{{i}}">
<label for="{{i}}"
style="font-size:14px;">{{ account.alias }}
</label>
</div>
Does not update the selected property of the underlying array.
This code:
<div *ngFor="let account of array2; let i = index;">
<input [(ngModel)]="account.selected"
type="checkbox"
name="genres"
id="{{i}}">
<label for="{{i}}"
style="font-size:14px;">{{ account.alias }}
</label>
</div>
<div>
Does update the selected property of the underlying array.
Use the first set of code if you want to ensure that the underlying array data is not changed.
But if you do need to track the changes as the user clicks and allow for a discard changes option, use the second set of code. Then in the component, copy the array to keep the original values:
ngOnInit() {
// Save a copy of the original values
this.array2Copy = this.array2.map(e => ({...e}));
console.log(JSON.stringify(this.array2Copy));
}
discardChanges() {
// Copy the original values over the array
this.array2 = this.array2Copy.map(e => ({...e}));
console.log(JSON.stringify(this.array2Copy));
}
I have a stackblitz of this code here:https://stackblitz.com/edit/angular-arraycopy-deborahk
I'm trying to set something up in my app where I can select an option from a list and change the background of the app depending on what's selected.
Let's say I have a list like:
<li v-for="item in items">
<label class="radio">
<input type="radio" value="{{ item.name }}" v-model="itemSelection">
{{ item.name }}
</label>
</li>
items is an array that's stored in my store.js:
items: [
{name: 'item1', img: 'placehold.it/200x200-1'}
{name: 'item2', img: 'placehold.it/200x200-2'}
{name: 'item3', img: 'placehold.it/200x200-3'}
],
So when you select item1 I want to not only pull the name from the selection (which gets passed up to the parent component in itemSelection to display there) but also the img link to place that in css to change the background of the body. I'm not entirely sure how to go about this, as I'm pretty new to vue and this is basically something I'm building to help me learn!
Thanks!
You can do this by several ways e.g:
watch : {
itemSelection: function(val) { ... }
}
There is some examples. Check this fiddle