create unique key in vue v-for loop - vue.js

following some research across the web, i understand i should add index to the loop and then add it as a key.
how would you suggest creating a unique key for both td's in the following code:
<template v-for="lesson in lessons">
<td #click="sort(lesson.questions)" :key="lesson.lessonId">
questions
</td>
<td #click="sort(lesson.grade)" :key="lesson.lessonId">
grade
</td>
</template>
the only idea i had was to add index to the loop and then have the second index as follows:
:key="`${lesson.lessonId}+1`"
but that feels a bit odd and error prone, am i right?

There are 2 ways,
first is add the static number as you mentioned:
:key="`${lesson.lessonId}567`"
Second is generate a new ID, and you will using uuid version 4 package, that will generate random id for you,
<template>
:key="generateID"
</template>
<script>
const uuidv4 = require('uuid/v4');
module.exports = {
data: function () {
return {
generateID: uuidv4();
}
}
}
</script>

The special attribute 'key' can be either 'numeric' or 'string', to solve the problem you can prefix your lessonId with a string
<template v-for="lesson in lessons">
<td #click="sort(lesson.questions)" :key="`question_${lesson.lessonId}`">
questions
</td>
<td #click="sort(lesson.grade)" :key="`grade_${lesson.lessonId}`">
grade
</td>
</template>`

<ul id="example-2">
<li v-for="(item, index) in items" :key="key(index, item)">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</ul>
var example2 = new Vue({
el: '#example-2',
data: {
parentMessage: 'Parent',
items: [
{ message: 'Foo' },
{ message: 'Bar' }
]
},
methods: {
key (index, item) {
return `${index}${item.message}`
}
}
})

Related

vue2 list return mixed string or component

In loop like this, I mostly just iterate over item values as strings, but sometimes need to return rendered component, for example build link element, or dropdown menu, for that table cell - need to find a way to return other component output instead of raw html string
<tr class="listing-item listing-item-category">
<td v-for="td in headeritems">{{val(td.k)}}</td>
</tr>
Is that even possible? I've found no mention of this, how should the method code go to return other component output? I know I would have to use v-html, but how to get it?
Assume we have a list like this:
headerItems: [
{
type: 'text',
value: 'Some text'
},
{
type: 'img',
props: {
src: 'http://some-where....'
}
},
{
type: 'my-component',
value: 'v-model value',
props: {
prop1: 10,
prop2: 'Blah bla',
},
events: {
myEvt: () => console.log('myEvt has fired')
}
},
],
So, We can render it:
<tr>
<td
v-for="(item, i) in headerItems" :key="i"
>
<div v-if="item.type === 'text'"> {{ item.value }}</div>
<component
v-else
:is="item.type"
v-model="item.value"
v-bind="item.props"
v-on="item.events"
/>
</td>
</tr>

Vue.js/Axios - Duplicate results in list. Has unique-keys in v-for

I have two other uses of v-for in separate components. They also sometimes throw errors. All three v-for invocations are wrapped with v-if/else. Here is the code that produces duplicate key errors & renders data twice:
AccountDashboard.vue
<tbody>
<tr v-if="!residents.length" class="table-info">
<td class="text-center">
<p>
No residents on record.
</p>
</td>
</tr>
<template v-else>
<tr is="AccountResidentList"
v-for="resident in residents"
v-bind:key="'resident-list-' + resident.id"
v-bind:first_name="resident.first_name"
v-bind:last_name="resident.last_name"
v-bind:dob="resident.dob | date_formatted"
>
</tr>
</template>
</tbody>
Note the unique id attempt in the binding of key.
Here is a look at the child component
ProviderAccountList.vue
<template>
<tr class="AccountResidentList">
<td>
{{ this.$attrs.id }}
</td>
<td>
{{ this.$attrs.first_name }} {{ this.$attrs.last_name }}
</td>
<td>
{{ this.$attrs.dob }}
</td>
<td>
<button #click="toResidentProfile({account_id, id})" class="btn btn-sm btn-purple btn-with-icon">
<div class="ht-25">
<span class="icon wd-25"><i class="fa fa-eye"></i></span>
<span class="pd-x-10">view</span>
</div>
</button>
</td>
<!--TODO: Add view profile button-->
</tr>
</template>
<script>
import Axios from "axios";
import router from "../../router";
import { mapGetters } from "vuex";
import moment from "moment";
export default {
name: "AccountResidentList",
computed: {
...mapGetters['Resident', {
resident: 'getResident'
}]
},
filters: {
date_formatted: (date) => {
return moment(date).format('MMMM Do, YYYY');
}
},
methods: {
toResidentProfile(account_id, resident_id) {
router.push(`/accounts/${account_id}/residents/${resident_id}`)
}
},
};
</script>
<style scoped></style>
My Axios call looks like:
Account.js (a namespaced vuex-module)
async retrieveAccount(context, account_id) {
// Axios.defaults.headers.common['Authorization'] = 'Bearer ' + window.$cookies.get('jwt')
let response
let valid_id = window.$cookies.get('valid_id');
response = await Axios.get(`http://localhost:3000/api/v1/providers/${valid_id}/accounts/${account_id}`, { headers: { 'Authorization': 'Bearer ' + window.$cookies.get('jwt') } })
.then((response) => {
let account = response.data.locals.account;
let account_address = response.data.locals.account_address;
let residents = response.data.locals.residents;
// set Account
context.dispatch('Account/setId', account.id, {root: true});
context.dispatch('Account/setProviderId', account.provider_id, {root: true});
.
.
.
// set AccountAddress
// !Array.isArray(array) || !array.length
if (account.address) {
context.dispatch('Account/setAddressId', account_address.id, {root: true});
context.dispatch('Address/setId', account_address.id, {root: true});
.
.
.
// set AccountResidents
// !Array.isArray(array) || !array.length
residents.forEach(resident => {
if (resident) {
// Add object to parent's list
context.dispatch('Account/setResidents', resident, {root: true}); // Set attr values for object
context.dispatch('Resident/setId', resident.id, {root: true});
.
.
.
(remaining attrs removed for brevity)
}
})
router.push(`/providers/${account.provider_id}/accounts/${account_id}`);
})
.catch(function(error) {
console.log(error);
})
Note: the Account action #setResidents simply calls the mutator that adds one resident to a list total.
i.e state.list.push(resident)
I logged the response to the console and can confirm that the data isn't being sent twice (or more) from my Axios call.
I have reviewed & attempted the following to no avail:
https://alligator.io/vuejs/iterating-v-for/
https://www.reddit.com/r/vuejs/comments/7n3zi4/vue_warn_duplicate_keys_detected_vfor_with/
https://github.com/hejianxian/vddl/issues/23
https://github.com/hejianxian/vddl#warning
https://medium.com/#chiatsai/vue-js-common-issue-duplicate-keys-stops-components-rendering-df415f31838e
Finally, It should be mentioned that I have tried variations of using/not using template to wrap the list, including/not including the for loop in the template, etc..
Did not anticipate it would be this bothersome to iterate a collection.
Am I overlooking something obvious?
Update: What worked for me
I needed access to the resident.id also the id declared in the paren seems like an index. So here is a look at what removed the duplicate render errors and allow me access to the resident's id even after fixing the duplicate keys error:
<template v-else>
<tr is="AccountResidentList"
v-for="(resident, id) in residents"
v-bind:key="id"
v-bind:id="resident.id"
v-bind:first_name="resident.first_name"
v-bind:last_name="resident.last_name"
v-bind:dob="resident.dob | date_formatted"
>
</tr>
</template>
Thanks again #Billal Begueradj for the assist!
For me, I suspect that in residents there are entries which have the same id. So we have to find out a way to overcome this issue. We can give it an efficient try as follows:
<tr
is="AccountResidentList"
v-for="(resident, id) in residents"
:key="id"
// rest of your code

V-model populated in a method not updating the DOM

I am newbie in VueJs.(vue 2). I have a problem here. I have a table where I am dynamically populating data like this.
<tbody>
<tr v-bind:key="queProduct.value" v-for="queProduct in queueProducts">
<td class="has-text-centered">
<figure class="image is-48x48">
<img :src="queProduct.image" alt="Placeholder image">
</figure>
</td>
<td><span>{{queProduct.title}}</span></td>
<td class="has-text-centered"><a class="has-text-link">
<span class="icon is-size-4" #click="openModalPopup(queProduct.id)">
<i class="fa fa-edit" />
</span>
</a>
</td>
<td class="has-text-centered"><a class="has-text-link">
<span class="icon is-size-4" #click="openModalPopup(queProduct.id)">
<img :src="queProduct.indicatorImg" />
</span>
</a>
</td>
<td class="has-text-centered"><a class="delete is-large has-background-link" #click="removeFromQueue(queProduct.id)"></a></td>
</tr>
</tbody>
methods:{
loadQueue(){
const indicators = store.get('productIndicators');
if(indicators === undefined){
store.set('productIndicators', []);
} else {
this.savedProprogressIndicators = indicators;
}
this.queueProducts.forEach(product => {
product.indicatorImg = indicatorImgBaseUrl +'Level-0.png';
this.savedProprogressIndicators.forEach(indicator => {
if(indicator.id === product.id){
product.indicatorImg = indicatorImgBaseUrl +indicator.image;
}
})
})
}
}
When I console.log the product, I see the product object being updated with the new value. But the dom isnt getting updated. Like,
this.product looks like this.
{
id: "d6dd8228-e0a6-4cb7-ab83-50ca5a937d45"
image: "https://zuomod.ca/image/cache/catalog/products/2018/Single/medium/50105-1-800x800.jpg"
inQueue: false
indicatorImg: "https://cdn.shopify.com/s/files/1/0003/9252/7936/files/Level-2.png"
saved: false
sku: "50105"
title: "Interstellar Ceiling Lamp"
}
But in the DOM, it looks like this
{
id: "d6dd8228-e0a6-4cb7-ab83-50ca5a937d45"
image: "https://zuomod.ca/image/cache/catalog/products/2018/Single/medium/50105-1-800x800.jpg"
inQueue: false
indicatorImg: "https://cdn.shopify.com/s/files/1/0003/9252/7936/files/Level-0.png"
saved: false
sku: "50105"
title: "Interstellar Ceiling Lamp"
}
Can you please help me resolve this?
Thanks,
Vandanaa
As you use Vuex, you should get your products directly from you store like in computed property in your vue definition. This will refresh the data directly from store without any action from vue side.
{
...
computed:{
...mapGetters({
queueProducts : 'queueProducts'
})
}
...
}
Furthermore, if your are using vuex, try to keep your logic inside your store. You vue should only display data.
Hava a look to vuex documentation to know when and where you should use
Getters, Mutations and Actions.
Hope this help.
this.queueProducts.forEach(product => {
...
...
...
}
this.$forceUpdate(); // Trying to add this code
I guessed your product.indicatorImg was not been watch by Vue, so it will not update the DOM. Trying to add this.$forceUpdate() in the end. It will force Vue to update DOM.

vue.js how to v-model values as separate arrays

from the backend I'm getting an array like this.
then I render this array to a table like this
My code
<tr v-for="item in items">
<td>
{{item[1]}}
</td>
<td>
{{item[2]}}
</td>
<td>
<input type="text" v-model="grnItems[items[1]]"/>
</td>
</tr>
This is a purchase return component
what I want is v-model this each an every input element as a separate array along with the item name.
like this
[
["chicken","12"]
["chille","19"]
]
How do I achieve this using vue.js?
Use an auxiliar array with the data populated the way you want, some example using computed properties
new Vue({
el: '#app',
data: {
items: [['1', 'some text', '66'], ['2', 'another text', '12'], ['5', 'random text', '89']],
result: []
},
computed: {
procesedItems() {
return this.items.map(i => ({
id: i[0],
name: i[1],
amount: i[2]
}))
}
},
methods: {
doSomething() {
this.result = this.procesedItems.map(i => {
let aux = [];
aux.push(i.name, i.amount)
return aux
})
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<ul>
<li v-for="item in procesedItems"> {{item.id }} {{item.name }} <input v-model="item.amount"/></li>
</ul>
<button #click="doSomething">Calculate</button>
{{ result }}
</div>

How to calculate the total in vue component ? Vue.JS 2

My vue component, you can see below :
<template>
<div>
<div class="panel-group" v-for="item in list">
...
{{ total = 0 }}
<tr v-for="product in item.products">
...
<td>
<b>Price</b><br>
<span>{{ product.quantity * product.price }}</span>
</td>
</tr>
{{ total += (product.quantity * product.price) }}
<tr>
<td colspan="3" class="text-right">
<b>Total: {{ total }} </b>
</td>
</tr>
</div>
</div>
</template>
<script>
export default {
...
computed: {
list: function() {
return this.$store.state.transaction.list
},
...
}
}
</script>
I try like above code
But, seems it still wrong
How can I solve it correctly?
I'm still newbie in vue.js 2
Since, TypeError: this.$store.state.transaction.list.reduce is not a function is an error marked in Frank's answer I presume this.$store.state.transaction.list is not an Array but an object as v-for iterates through both.
total: function() {
var list = this.$store.state.transaction.list
var sum = 0
for(var listProps in list) {
list[listProps].products.forEach(function (product) {
sum += product.pivot.quantity * product.pivot.price
})
}
return sum;
}
Use another computed property
<script>
export default {
...
computed: {
list: function() {
return this.$store.state.transaction.list
},
total: function() {
return this.$store.state.transaction.list.reduce(function(sum, item) {
sum += item.products.reduce(function(tmp, product) { tmp += product.quantity * product.price; return tmp; }, 0);
return sum;
}, 0);
}
...
}
}
</script>
Use a nested Array.reduce to get the total of your structure where the list has many items and an item has many products