React to object property change in an Array in Vue 3 - javascript

I have a Vue 3 app. In this app, I need to show a list of items and allow a user to choose items in the array. Currently, my component looks like this:
MyComponent.vue
<template>
<div>
<div v-for="(item, index) in getItems()" :key="`item-${itemIndex}`">
<div class="form-check">
<input class="form-check-input" :id="`checkbox-${itemIndex}`" v-model="item.selected" />
<label class="form-check-label" :for="`checkbox-${itemIndex}`">{{ item.name }} (selected: {{ item.selected }})</label>
</div>
</div>
<button class="btn" #click="generateItems">Generate Items</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
data() {
return itemCount: 0
},
methods: {
generateItems() {
this.itemCount = Math.floor(Math.random() * 25) + 1;
},
getItems() {
let items = reactive([]);
for (let i=0; i<this.itemCount; i++) {
items.push({
id: (i+1),
selected: false,
name: `Item #${i+1}`
});
}
return items;
}
}
}
</script>
When I click select/deselect the checkbox, the "selected" text does not get updated. This tells me that I have not bound to the property properly. However, I'm also unsure what I'm doing wrong.
How do I bind a checkbox to the property of an object in an Array in Vue 3?

If you set a breakpoint in getItems(), or output a console.log in there, you will notice every time that you change a checkbox selection, it is getting called. This is because the v-for loop is re-rendered, and it'll call getItems() which will return it a fresh list of items with selected reset to false on everything. The one that was there before is no longer in use by anything.
To fix this, you could only call getItems() from generateItems() for example, and store that array some where - like in data, and change the v-for to iterate that rather than calling the getItems() method.

Steve is right.
Here is a fixed version: Vue SFC Playground.
<template>
<div>
<div v-for="(item, index) in items" :key="`item-${index}`">
<div class="form-check">
<input type="checkbox" class="form-check-input" :id="`checkbox-${index}`" v-model="item.selected" />
<label class="form-check-label" :for="`checkbox-${index}`">{{ item.name }} (selected: {{ item.selected }})</label>
</div>
</div>
<button class="btn" #click="generateItems">Generate Items</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
data() {
return { items: [] }
},
methods: {
generateItems() {
this.itemCount = Math.floor(Math.random() * 25) + 1;
this.getRndItems();
},
getRndItems() {
this.items = reactive([]);
for (let i=0; i<this.itemCount; i++) {
this.items.push({
id: (i+1),
selected: false,
name: `Item #${i+1}`
});
}
}
}
}
</script>

Related

splice() wrong component in data array

Let's say I have a form, and can add or remove a column by a click.
I use v-for to render the vue components, when I try to use splice() to delete a specific component, it always delete the last component in the array.
I can't figure out what I did wrong here, any hint will be very appreciated.
Here is a part of my code:
the problem is occured at removePerson method.
Parent Component
<template>
<div class="form-container">
<template>
<div v-for="(child, key) in otherPersonArray" :key="key" class="form-container">
<button #click="removePerson(key)" class="close">X</button>
<component :is="child"></component>
</div>
</template>
<button #click="addPerson" >+ Add Person</button>
</div>
</template>
<script>
import otherPerson from './OtherPerson';
export default {
components: {
otherPerson
},
data() {
return {
otherPersonArray: [],
}
},
methods: {
addPerson() {
if (this.otherPersonArray.length <= 10) {
this.otherPersonArray.push(otherPerson);
}
},
removePerson(key) {
this.otherPersonArray.splice(key, 1);
},
},
}
</script>
For example, when I try to delete the component which input value is 1, it delete the component which input value is 2.
otherPerson component
<template>
<div class="form-container">
<div class="person">
<div class="row">
<div class="form-group col-6">
<label for="inventor-last-name">Lastname of Person *</label>
<div class="input-container">
<input v-model="lastName" type="text" class="form-control form-control-lg">
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
lastName: '',
}
},
}
</script>
You can use Array.prototype.filter
removePerson(key) {
this.otherPerson = this.otherPersonArray.filter((x, i) => i !== key);
}
there are couple of ways to achieve this but for now can you try these:
removePerson(key) {
this.otherPersonArray = this.otherPersonArray.filter(person => {
return person.key === key;
})
}
OR
const index = this.otherPersonArray.indexOf(ele by key); // get index by key person key
if (index > -1) {
this.otherPersonArray.splice(index, 1);
}
What I am understanding key is index here then you should follow this:
var filtered = this.otherPersonArray.filter(function(value, index){
return index !== key;
});
let me know if still it not working for you?
Here is example :

How set computed property of checked checkboxes via v-model?

I've looked several resources, but I don't find the solution:
https://v2.vuejs.org/v2/guide/computed.html
V-model with props & computed properties
vuejs v-model, multiple checkbox and computed property
I've a table with people that have roles, if I want to change the roles I open a modal.
<template>
<div>
<b-table small outlined striped hover :items="peoplePaginated.data" :fields="fields" >
<template slot="actions" slot-scope="row">
<b-button size="sm" class="my-2 my-sm-0 btn-outline-info" v-b-modal="'federation-role-modal'" #click.stop="change(row.item)">
edit
</b-button>
<b-button size="sm" #click.stop="info(row.item, row.index, $event.target)" class="btn btn-outline-success btn-sm">
details
</b-button>
</template>
</b-table>
<FederationRoleModal :roles="roles" />
</div>
</template>
data () {
return {
roles: [],
}
},
methods: {
info (item) {
this.$router.push({ name: 'person', params: { id: item.id }})
},
change (person) {
const roles = person.personRoles.map(el => el)
const allRoles = roles.map(el => el.roleCategory.name)
this.roles = allRoles
}
}
Then I've a list of checkboxes where the checkedRoles takes care of the checked ones. When I click on a new checkbox, I want the data property to be updated. However this updating does not happen.
In the modal:
<span v-for="value in allRoles" :key="value.id">
<input type="checkbox" :value="value" v-model="checkedRoles">
<span class="checkbox-label"> {{value}} </span> <br>
</span>
computed property: {
checkedRoles: {
get: function () {
// console.log(this.roles)
return this.roles
},
set: function (newValue) {
// console.log(newValue)
return newValue
}
}
}
this.roles comes from the parent component (array with roles).
I do see the console.log(newValue) output as a new array with an additional role, but this new array is not visible as the checkedRoles data property.
[option 2] I've also tried to add checkedRoles: this.roles, in the data () { return {... }. But when I open several times a modal, the roles of the previous clicked row.item are still in the data property.
Please advise
if I should be using a computed property, or dig deeper into [option 2]
how I get all checked checkboxes to be in the checkedRoles data property.
checkRoles should be an empty array in your data object like :
data(){
return{
checkedRoles:[]
...
}
}
<span v-for="value in allRoles" :key="value.id">
<input type="checkbox" :value="value" v-model="checkedRoles">
<span class="checkbox-label"> {{value}} </span> <br>
</span>
Solution included modifying the parent component data property:
In the parent component add:
<FederationRoleModal :checked-roles="roles" #update-role="onUpdateRole"/>
methods:
onUpdateRole(value) {
let rolesArray = this.roles
if (rolesArray.includes(value)){
var index = rolesArray.indexOf(value)
if (index > -1) {
return rolesArray.splice(index, 1);
}
} else {
return rolesArray.push(value)
}
}
in the child component
<span v-for="value in allRoles" :key="value.id">
<input type="checkbox" :value="value" v-model="roles" #click="$emit('update-role', value)">
<span class="checkbox-label"> {{value}} </span> <br>
</span>
data:
props: ['checkedRoles'],
computed:
roles: {
get: function () {
return this.checkedRoles
},
set: function (newValue) {
return newValue
}
}

Vue.js checkbox component multiple instances

I have a list of filters using checkboxes. I'm trying to make each checkbox it's own components. So I loop through my list of filters adding a checkbox component for each filter. The Vue.js documentation says that if I have multiple checkboxes that use the same model that array will get updated with the value of the checkboxes. I see that working if the group of checkboxes is part of the parent component. But if I make the checkbox a component and add each checkbox component in a loop then the model doesn't update as expected.
How can I have a checkbox component that updates an array on the parent? I know I can do this with emitting an event for a method on the component that updates the array but the Vue documentation makes it seems like the framework does this for you.
Here is a code sample I've been playing around with https://www.webpackbin.com/bins/-KwGZ5eSofU5IojAbqU3
Here is a working version.
<template>
<div class="filter-wrapper">
<input type="checkbox" v-model="internalValue" :value="value">
<label>{{label}}</label>
</div>
</template>
<script>
export default {
props: ['checked','value', 'label'],
model: {
prop: "checked"
},
computed:{
internalValue: {
get(){return this.checked},
set(v){this.$emit("input", v) }
}
}
}
</script>
Updated bin.
The answer given by #Bert is right. I just want to complete the picture with the list of components and how thay are integrated. As this is a useful pattern.
Also including Select All functionality
ListItem.vue
<template>
<div class="item">
<input type="checkbox" v-model="internalChecked" :value="item.id" />
... other stuff
</div>
</template>
<script>
export default {
// Through this we get the initial state (or if the parent changes the state)
props: ['value'],
computed:{
internalChecked: {
get() { return this.value; },
// We let the parent know if it is checked or not, by sending the ID
set(selectedId) { this.$emit("input", selectedId) }
}
}
}
</script>
List.vue
<template>
<div class="list">
<label><input type="checkbox" v-model="checkedAll" /> All</label>
<list-item
v-for="item in items"
v-bind:key="item.id"
v-bind:item="item"
v-model="checked"
</list-item>
... other stuff
</div>
</template>
<script>
import ListItem from './ListItem';
export default {
data: function() {
return: {
// The list of items we need to do operation on
items: [],
// The list of IDs of checked items
areChecked: []
}
},
computed: {
// Boolean for checked all items functionality
checkedAll: {
get: function() {
return this.items.length === this.areChecked.length;
},
set: function(value) {
if (value) {
// We've checked the All checkbox
// Starting with an empty list
this.areChecked = [];
// Adding all the items IDs
this.invoices.forEach(item => { this.areChecked.push(item.id); });
} else {
// We've unchecked the All checkbox
this.areChecked = [];
}
}
}
},
components: {
ListItem
}
}
</script>
Once boxes get checked we have in checked the list of IDS [1, 5] which we can use to do operation on the items with those IDs

uncheck all checkboxes from child component Vue.js

I have the following problem: I have parent component with a list of checkboxes and two inputs. So when the any of those two inputs has been changed I need to uncheck all checkboxes. I would appreciate if you can help me to solve this.
I wanted to change checkedItem to trigger watch in child and then update all children, but it doesn't work.
parent.vue
<template>
<div class="filter-item">
<div class="filter-checkbox" v-for="item in filter.items">
<checkbox :item="item" v-model="checkedItem"> {{ item }} </checkbox>
</div>
<div class="filter-range">
<input v-model.number="valueFrom">
<input v-model.number="valueTo">
</div>
</div>
</template>
<script>
import checkbox from '../checkbox.vue'
export default {
props: ['filter'],
data() {
return {
checkedItem: false,
checkedItems: [],
valueFrom: '',
valueTo: '',
}
},
watch: {
'checkedItem': function () {},
'valueFrom': function () {},
'valueTo': function () {}
},
components: {checkbox}
}
</script>
child.vue
<template>
<label>
<input :value="value" #input="updateValue($event.target.value)" v-model="checked" class="checkbox"
type="checkbox">
<span class="checkbox-faux"></span>
<slot></slot>
</label>
</template>
<script>
export default {
data() {
return {
checked: ''
}
},
methods: {
updateValue: function (value) {
let item = this.item
let checked = this.checked
this.$emit('input', {item, checked})
}
},
watch: {
'checked': function () {
this.updateValue()
},
},
created: function () {
this.checked = this.value
},
props: ['checkedItem', 'item']
}
</script>
When your v-for renders in the parent component, all the rendered filter.items are bound to the same checkedItem v-model.
To correct this, you would need to do something like this:
<div class="filter-checkbox" v-for="(item, index) in filter.items">
<checkbox :item="item" v-model="item[index]> {{ item }} </checkbox>
</div>
To address your other issue, updating the child component list is as easy as updating filter.items.
You don't even need a watcher if you dont want to use one. Here is an alternative:
<input v-model.number="valueFrom" #keypress="updateFilterItems()">
And then:
methods: {
updateFilterItems () {
// Use map() or loop through the items
// and uncheck them all.
}
}
Always ask yourself twice if watch is necessary. It can create complexity unnecessarily.

Vue.js how to delete component v-for values

I am learning Vue, so I created radio button component, but I am struggling with how can one delete these values. My current solution deletes actual values, but selection is still displayed.
This is the component
<template id="fradio">
<div>
<div class="field is-horizontal">
<div class="field-label" v-bind:class = "{ 'required' : required }">
<label
class = "label"
>{{label}}
</label>
</div>
<div class="field-body">
<div>
<div class="field is-narrow">
<p class="control" v-for="val in values">
<label class = "radio">
<input
type="radio"
v-bind:name = "name"
v-bind:id = "name"
#click = "updateValue(val)"
>
<span>{{val[valueLabel]}}</span>
<span v-if="!valueLabel">{{val}}</span>
</label>
<label class="radio">
<button class="delete is-small" #click="removeValue"></button>
</label>
<slot></slot>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
required: true,
},
inputclass: {
type: String,
},
required: {
type: Boolean,
default: false,
},
valueLabel:{
type: String,
},
returnValue:{
type: String,
},
values:{},
name:'',
},
data() {
return {
};
},
methods: {
updateValue: function (value) {
var selectedValue;
(!this.returnValue) ? selectedValue = value : selectedValue = value[this.returnValue];
this.$emit('input', selectedValue)
},
removeValue: function() {
this.$emit('input',null);
},
},
}
</script>
It should be easy, but I need someone to point out the obvious...
Update:
I just realized that you may be more focused on the data not dynamically updating, which means that your issue might be that the data in the parent component is not being updated. Most of your data is being passed down as props, so I'd need to see how the event is being fired in the parent component in order to help diagnose what's wrong. Based on the code you provided, it looks like your removeValue() function is emitting an event but I don't see any code that actually removes the value.
I would check the parent component to make sure that it is removing the child component and that should fix your problem!
Initial Answer:
Generally, when removing an item from a v-for list, you need to know the index of the item and use the Array.splice in order to modify the list to remove it.
Here's a generic example off the top of my head.
<template>
<ul>
<li v-for="(fruit, index) in fruits"
#click="removeItem(index)">
{{ fruit }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
fruits: ['Apple', 'Banana', 'Clementines']
}
},
methods: {
removeItem(index) {
this.fruits.splice(index, 1)
}
}
}
</script>
Let me know if you have any questions!

Categories

Resources