In my application I need to use a nested v-for to display a list of elements with a select-option.. This is the scenario
<div class="stuck" v-for="box in items">
<p>Pick an option for this box:</p>
<select v-model="box">
<option v-for="package in packages"
:value="package.id">{{ package.name }} </option>
</select>
</div>
The variable items come from Vuex store. In this way, i'm getting the error:
You are binding v-model directly to a v-for iteration alias. This will
not be able to modify the v-for source array because writing to the
alias is like modifying a function local variable. Consider using an
array of objects and use v-model on an object property instead.
With this in mind, i'm going to change the code like so:
<div class="stuck" v-for="box in items">
<p>Pick an option for this box:</p>
<select v-model="box.id">
<option v-for="package in packages"
:value="package.id">{{ package.name }} </option>
</select>
</div>
I've just changed the select v-model from the alias box, to the right id: box.id
In this way, all works... or... half works. Because, if i'm going to pick an option from the select, i got another error:
[vuex] Do not mutate vuex store state outside mutation handlers.
This is correct, because the v-model is bind to box.id (that is not an alias but a real value). But, when i pick an option the v-model "try" to change box.id that come from Vuex store.
Now, in a simple scenario i will create a computed property for set/get to avoid vuex store mutation.
But... here i have a nested loop, so i cant create a computed on 'box.id'.
Do you have a solution for this ?
Thanks a lot!
you could try a different data flow pattern.
Your select listens to the store (but does not directly update it)
<div class="stuck" v-for="box in items">
<p>Pick an option for this box:</p>
<select :value="box.id" #change="updateBox">
<option v-for="package in packages" :value="package.id">
{{ package.name }}
</option>
</select>
</div>
Then you create a method that fires whenever the selected option changes
updateBox(e) {
const id = e.target.value;
this.$store.commit('changeYourBox', id);
},
This function should commit a vuex mutation that alteres the box id. So you'd need that mutation too.
Once the store value updates, your components box object updates and the select that listens to that store will update it's selected value accordingly.
That way, you can alter the store value from anywhere and the selected value will change as well.
with usage of mine library vuex-dot in this situation you can do so:
let's go with such state
{
state: {
boxes: []
},
mutations: {
editBox(state, {target, key, value}) {
Vue.set(target, key, value);
}
}
};
So let's create additional component BoxEdit:
<template>
<div class="stuck">
<p>Pick an option for this box:</p>
<select v-model="id">
<option v-for="package in packages"
:value="package.id">{{ package.name }} </option>
</select>
</div>
</template>
<script>
import { take } from 'vuex-dot'
export default {
props: ['box', 'packages'],
computed: {
...take('box')
.expose(['id'])
.commit('editBox', true)
.map()
}
}
</script>
and now you can make simply write
<box-edit v-for="box in boxes" :box="box" :packages="packages"></box-edit>
in your subject component template.
link to library site: https://github.com/yarsky-tgz/vuex-dot
Related
I have many select boxes that have many options and are dynamic.
<template>
<div>
<tr v-for="category in categories.items" :key="category.id">
<td>
<select v-model.trim="$v.form.attributeValues.$model" class="form-control">
<option value=""></option>
<option v-for="option in category.values" :key="option.id" :value="option">
{{ option.content }}
</option>
</select>
</td>
</tr>
</div>
</template>
<script>
export default {
data() {
return {
categories: null,
form: {
productId: this.product.productId,
attributeValues: [],
},
}
},
}
</script>
I want get the values of theme and save it in an array.
But it doesn't work and I can only save 1 option.
like this:
(get just one option from each selectbox)
A multi-select is far from being a trivial a trivial component.
Most people simply use vue-multiselect or any package alike.
There is also a ton available only. You should look for the one matching your wanted features.
If you want to implement it manually, you will need to deal with binding the proper inputs, events and structure since it is not achievable with a simple v-model.
One of the best article on this is this one.
Still, it all depends on how you want it to look like, the behavior of the various options, the way to select it (keyboard, ctrl + click, simple click etc...), the transitions etc...
TLDR: use a package or try it yourself and show us what you achieved so far if you want some debugging.
The community will not write a complex component from scratch for you tho.
Just put a v-model and a input-event to your option like this:
template
<option v-model="selectedItem" #input="checkItem(selectedItem)">
<!-- Your code -->
</option>
Than go to your script and define everything. After that you can make a method called checkItem and there you will push everything in a defined array which you have selected.
script
data() {
return {
selectedItem: null,
allItems: [],
}
},
methods: {
checkItem(selectedItem) {
this.allItems.push(selectedItem)
}
}
Hopefully this helps you out - pls let me know!
What or where is individual id or any other element that I can call to run a function on component.
Example: using add button I'm dynamically creating colored squares. Each square has close button.
Then I want to delete one of squares, let's say the blue one (third child of template).
v-bind:id='var' doesn't works because then all squares have the same id.
You can use ref, you can define ref in every added component. The ref is array. you can refrence component through index of the ref.
for example;<div v-for="(item) in arr" :key="item.id"> <span ref="testRef"> item.name </span> </div>, you can use 'this.$refs.testRef[index]' find component which you want.
There are multiple options. For instance, in your v-for loop you can get the index this way: (official docs)
<div v-for="(item, index) in myList" :key="index">
{{ item.name }}
</div>
You could then for example use the index in a ref to access the element for the script.
Another option is to pass the event instance to the onclick listener with the special keyword $event, and use this to get the element:
<div ... #click="myFunction($event)">
{{ item.name }}
</div>
And then in your script
myFunction(ev) {
// do something with the element that was clicked on
console.log(ev.target)
}
However, a more 'vue' way would be to not mess with the elements, but only manipulate the underlying data and let vue worry about representing the data with elements on your page. For instance, you could make your squares be represented by a list of objects, containing their own data own the state (opened or closed). So in your script you have:
data() {
return {
squares: [
{ color: "ff0000", opened: true },
...
]
}
},
And in your template something like
<div
v-for="square in squares"
v-show="suare.opened"
#click="square.opened = false"
> ...
I'm trying to make changes to an array in the parent component (insert and update data) that is passed to the child component via Props, but the DOM is not being updated.
Parent component:
<UsersList
v-for="(role, i) in userRolesNames"
:key="i"
:users="usersPages[i].data"
/>
Child component:
<template>
<div
v-for="user in users"
:key="user.id"
>
<span>{{ user.name }}</span>
<div>
<i
class="bi bi-pen-fill edit-icon m-pointer"
#click="onClickEdit(user)"
></i>
<i
class="bi bi-trash-fill delete-icon ms-2 m-pointer"
#click="onClickDelete(user)"
></i>
</div>
</div>
</template>
<script lang="ts">
// recieving the array
props: {
users: {
required: true,
type: Array as PropType<usersType[]>
}
}
</script>
Each user basically has the following structure:
{
id: x,
name: 'x',
email: 'x',
login: 'x',
role: x
}
The problem is that when trying to insert or update a record in the usersPages[i].data array, the DOM doesn't change. If I use the Vue developer tools, the data is changing correctly, but the DOM isn't.
Tried inserting using the push method on the array but without success. The only thing that worked was this:
const newUser = response.data;
const page = this.usersPages[newUser.role - 1];
Vue.set(page, 'data', [...page.data, newUser]);
To update I tried to directly change the user properties, but like the insert the DOM doesn't update. What worked was:
const page = this.usersPages[user.role - 1];
const oldUsers = page.data.filter(u => u.id != user.id);
Vue.set(page, 'data', [ ...oldUsers, response.data ]);
Works but doesn't look right... Can anyone help?
I had a similar problem before. I believe that this is a bug in vue. The reason for this is that Vue rendered the v-for already. Vue as of now does not know how to handle the changes in the array. What I did as a workaround for this is have a updateKey variable inside my script set to 0. Then increment this variable every time we update the array updateKey++. And we use this key for our component.
On child component,
<template>
<span
v-for="user in users"
:key="user.id"
>
<div :key="updateKey">
<span>{{ user.name }}</span>
<div>
<I
class="bi bi-pen-fill edit-icon m-pointer"
#click="onClickEdit(user)"
></i>
<I
class="bi bi-trash-fill delete-icon ms-2 m-pointer"
#click="onClickDelete(user)"
></i>
</div>
</div>
</span>
</template>
and onClickEdit(user) and onClickDelete(user) make sure you have updateKey++
The DOM won't update because you are not doing it reactively. To update this value reactively you should listen to some event (input for example), that you'll emit in the child component and pass the new(!) value to your property. But a better way is to bind your components via v-model.
P.S. - So you can see changes in the devtools because this is not a part of the Vue lifecycle, it`s just like a parser that watches actual data, but without context.
I decided to create an example to show #peperoneen how I did it, as I believed the way was correct. But in the example itself, it worked and in my project, it didn't, and the only difference was the way the list was initially filled.
To load the users I do a request to the API that returned the paged data, which I normally assigned with the '=' operator. However, I decided to do the assignment step by step and in the users part, I used the 'push' method. This way, the operations with the list using push and filter work normally, updating the DOM and without the need to use Vue.set
I think the problem was the way the list was being generated. I wasn't doing it reactively I guess...
v-model on tag doesnt not reflect any changes in the blade template.
I have tried this with an empty html file but its working, but only got the problem in blade template
//blade
<div id="fruit-container">
<select v-model="fruit">
<option disabled value="">Choose a fruit</option>
<option>Apple</option>
<option>Banana</option>
<option>Strawberry</option>
</select>
<span>Fruit chosen: #{{fruit}}</span>
</div>
//js
new Vue({
el:'#fruit-container',
data:{fruit:""}
})
the value #{{fruit}} never show up when i select the different option, and this is only not working in blade i am wondering what did i do wrong here?
I think you need to change your data to this:
data() {
return { fruit: '' }
}
Vue.js has an example demonstrating how to build a functional component wrapping a button:
<template functional>
<button
class="btn btn-primary"
v-bind="data.attrs"
v-on="listeners"
>
<slot/>
</button>
</template>
But how can I build a similar component working with select which I can then use v-model on its parent?
I have come up with the following, but when I use v-model on it like so:
<MyComponent :Label="'Status'" :Data="Statuses" v-model="selectedStatus" />
The value of selectedStatus becomes:
[object Event]
<template functional>
<select v-bind:value="data.attrs" v-on="listeners">
<option>-</option>
<option v-for="item in props.Data" :key="item" :value="item">{{item}}</option>
</select>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
#Component
export default class TaskSelector extends Vue {
#Prop({ type: String, required: true })
public Label!: string;
#Prop({ type: Array, required: true })
public Data!: string[];
}
</script>
For v-model to work properly, the component must receive a value prop and emit an input event whose data is a string value. But the <select>'s native input event data is an InputEvent object, converted to a string ([object Event]), which yields the incorrect result with the bound v-model.
To fix this, we need to modify the <select>'s input event data before it reaches the parent. This requires removing <select v-on="listeners">, which would've allowed the problematic native input event to propagate to the parent. We then use listeners.input($event.target.selectedOptions[0].value) to forward the input event with the selected option's string value.
Steps:
Add a value prop, and bind the <select>'s :value to the value prop
Re-emit the <select>'s input event with the selected option's value. This should replace v-on="listeners".
Your SFC should look like this:
<template functional>
<select
1️⃣:value="props.value"
2️⃣#input="listeners.input && listeners.input($event.target.selectedOptions[0].value)"
>
<option value="">-</option>
<option v-for="item in props.Data" :key="item" :value="item">{{item}}</option>
</select>
</template>
demo