Any problems with defining variables inside v-for? - vue.js

AFAIK, first shown here by Vladimir Milosevic
<div
v-for="( id, index, user=usersData[id], doubleId=doubleThat(id) ) in users"
:key="id">
{{ user.name }}, {{ user.age }} years old And DoubleId = {{ doubleId }}
</div>
I expanded his codepen. Looks like you can define several variables local to the loop this way.
Are there any side-effects or risks you see with this technique?
I find this way to be the most convenient and clean out there but since that is apparently not the intended use it may not be future-proof.
EDIT:
Here is another pen which demonstrates the use case better. While here we reference just one computed property in one place, if the number of properties and references grows, it will lead to a verbose code.

In general, I think the clean way to do what you want to do is to bring the data into a format, where you can loop over it without extra steps. This way, you will never need those additional variables in v-for.
The example from your codepen could look like this:
const app = new Vue({
el: '#app',
data: {
years: [2024, 2025, 2120],
usersData: {...}
},
computed: {
// user objects for each year
ageData() {
return Object.fromEntries(this.years.map(year => [year,
Object.values(this.usersData).map(user => this.userInYear(user, year))
] ))
}
},
methods: {
userInYear(user, year){
return {...user, age: user.age + (year - 2023)}
}
},
});
Now you can iterate it without interruption and shenanigans:
<div v-for="(users, year) in ageData" :key="year">
<b>In {{ year }}:</b>
<div v-for="user in users" :key="user.name">
{{ user.name }} will be {{ user.age }} years old.
</div>
</div>
It is easier to read and change, and it has the added benefit that you can inspect ageData in the console.
That said, I think it is very interesting that you can declare additional variables in v-for, I assume it is possible because of the way vue parses and builds expressions from the provided string, I am pretty sure it won't be removed, and if you and the people you work with are comfortable with it (and are prepared to hear the above over and over like just now), by all means, go for it.

Related

Ionic Vue Checkboxes

I have the following Problem:
I'm using Ionic Vue and a VueX Store to save my data from an API.
Now I have set an array, which contains the IDs of entries, which shall be checked or unchecked.
Since I should not modify the API-Model class, I have saved the IDs of checked entries in a seperate Array in my VueX Store, which I update as needed.
Now I'm trying to make the checkboxes checked / unchecked depending on that array.
I tried it by adding v-model = "checkedVehicles.included(vehicle.vehicle_id)", but all I get is an Error:
'v-model' directives require the attribute value which is valid as LHS vue/valid-v-model
Heres the Part whit the checkboxes, hope that is all you need :)
<IonItem v-for="vehicle in vehicleList" v-bind:key="vehicle.vehicle_id">
<IonLabel>
<h2>{{ vehicle.manufacturer }} {{ vehicle.model }}</h2>
<p>{{ vehicle.display_name }}</p></IonLabel>
<IonCheckbox slot="end"
v-model="checkedVehicles.includes(vehicle.vehicle_id)"
#click="checkIfAllDeselected"
#update="this.updateCheckboxOnClick(vehicle.vehicle_id)"/>
</IonItem>
The checkedVehicles Arrays is intialized as String[].
Also tried to use a function, which returns true or false, depending on the checkedVehicles Array has the ID included or not, but that also gives the same error
The other functions, which add or remove entires to the correspondig arrays are working fine, already checked that. only the Checkboxes are not working as intended.
Has anyone a clue, what I'm doing wrong?
This is obvious because we can't evaluate a condition in v-model. We generally bind a variable to the v-model.
For eg:
Consider you have a attr called vehicle in data.
data() {
return {
Vehicle list: [{
checked: true,
manufacturer: '',
display_name: '',
vehicle_id: ''
},
{
checked: true,
manufacturer: '',
display_name: '',
vehicle_id: ''
},
]
}
then you can bind it as
<IonItem v-for="vehicle in vehicleList" v-bind:key="vehicle.vehicle_id">
<IonLabel>
<h2>{{ vehicle.manufacturer }} {{ vehicle.model }}</h2>
<p>{{ vehicle.display_name }}</p></IonLabel>
<IonCheckbox slot="end"
v-model="vehicle.checked"
#click="checkIfAllDeselected"
#update="this.updateCheckboxOnClick(vehicle.vehicle_id)"/>
</IonItem>
To conclude, variables that can hold value can only be used in v-model

VueJS: computed property using another computed property isn't working correctly

this is not these: post1, post2
i'm building a small SPA and so i started with this basic principle that i tested and built up into the code farther below that i'm having problems with. it works ONLY if i call the secondary property in the DOM (why??) and i've included it to illustrate where i started from, hoping that the principles would translate as i built up.
JS:
var app = new Vue({
el: '#app',
computed: {
today: function() {
return new Date().getTime();
},
tomorrow: function() {
return this.today + (1*24*60*60*1000);
},
});
DOM:
<div id='app'>
<p>{{ today }}</p>
<p>{{ tomorrow }}</p>
</div>
so. that works. again, ONLY if app.tomorrow is called in the DOM. (wish i knew how NOT to need that...) BUT my problem is this next part, which i based on this starter code. it DOESN'T work:
JS:
var app = new Vue({
el: '#app',
data: {
dueNow: 0,
reports: { 90: [], 'DN': [] },
},
computed: {
today() {
return new Date().getTime();
},
tMinus30() {
return new Date().getTime() - (30 * 24 * 60 * 60 * 1000);
},
dueNow() {
var count = 0;
$.each(this.rows, function(index, obj) {
var workingDate = new Date(obj.dateSubmitted).getTime();
console.log('workingDate: '+workingDate);
console.log('today: '+this.today); <-- console reports 'undefined'!?
console.log('today: '+this.tMinus30); <-- console reports 'undefined'!?
if(workingDate < this.today && workingDate > this.tMinus30){
console.log('knock, knock: due NOW'); <-- therefore this never fires
count++;
console.log('count: '+count);
// build reports array - doesn't work, but that's for another day...
this.reports['DN']['count'] = push(count);
this.reports['DN'][index]['reportID'] = push(obj.id);
this.reports['DN'][index]['assetID'] = push(obj.assetid);
}
});
return count;
},
}
});
DOM:
<div id='app'>
<div class="col-lg-2">
<h1 class="text-center pt-2">NOW</h1>
<p>{{ today.getDay() }}</p>
<p class="img-text mb-1" #click="expandInfo('DN')">{{ dueNow }}</p>
</div>
</div>
so not only am i not getting the count of rows that are dueNow, i'm not getting today.getDay() calculated either, which, based on this from VueJS, should work. i suspect this is a flying nightmare, but i don't see it... it DOES iterate over the rows properly. it DOES calculate the working date for each row. but nothing else works. i thought i understood VueJS well enough to attempt this. and i even built it up from the first example... but i'm at a loss as to what's broken.
what principle am i overlooking to make this gel? it feels like some concept has eluded me.
EDIT: i also perused this article but don't know that it applies as there's nothing that isn't declared in the data already...
EDIT: OK! :) i need to clarify. when i say "it doesn't work", what i mean is the computed property tomorrow does not render and reports the other computed property in the console as "undefined", despite it (today) rendering just fine (without the additional method). my expected/desired result is that i will see a number indicating how many rows passed muster and are dueNow and i want to see the today rendered as "Monday" and tomorrow as "Tuesday" (for example). this last should be interchangeable with other date functions. if possible, i'd also like to have the array reports update properly... but that's out of scope right now.

For each item in a Vuex Store, assign a score (based on a calculation), then sort all by score

I'm trying to get my head around how to best implement the following requirements into a Vuex (vue.js) application.
I'll try and keep it as simple as possible, what i'm looking to achieve is something similar to Hacker News site. Therefore application will need to calculate a score for each item (post) based on a calculation/algorithm. The score is determined by a combination of likes and the date it was created on. Therefore the scores are constantly updating (until a designated time period expires) therefore it the score is not stored in a data source but calculated by the app.
So my main question is, how best to go about implementing this for a Vuex application?
I have set up a Vuex store of posts (i've called each article a 'post') and I'm able to display all the posts in a list. So now i've have two main objectives:
Assign a score to the posts (articles)
Filter the posts results list by their scores.
I've done some research and the consensus feels like it would be best to keep the 'score' separate from the 'posts' store (not added to the 'posts' array).
The following code works fine, however feels verbose. Would it be better to move any of these functions to 'getters/mutations/actions'?
Please note: for this examples i've simplified the calculation/algorithm to 'likes' total plus 'comments' total, obviously the final algorithm is more complicated to work like hacker news.
<div>
<button #click="postFilterKey = 'all'"><button>
<button #click="postFilterKey = 'orderByScore'"><button>
</div>
<div v-for="post in filterPosts" v-bind:key="post.value">
<p>Title: {{ post.title }}</p>
<p>Total Likes: {{ post.likes }}</p>
<p>Total Comments: {{ post.comments }}</p>
<p>Score: {{ post.comments | getScore(post.likes) }}</p>
</div>
computed: {
...mapState(["posts"]),
filterPosts () {
return this[this.postFilterKey]
},
all () {
return this.posts;
},
orderByScore (){
return this.posts.sort((b,a) =>
this.calculateScore(a.totalComments, a.totalLike) -
this.calculateScore(b.totalComments, b.totalLike));
},
methods: {
calculateScore(totalComments, totalLikes) {
let score = totalComments + totalLikes
return score;
},
}
filters: {
getScore(totalComments, totalLikes) {
let score = totalComments + totalLikes
return score;
},
}
Thanks for having a look.
Like you said, what you have will work fine, and there is nothing wrong with it, per se.
There are a couple issues with your current code that could be improved:
If you want to reuse the score anywhere else in your app that is not a direct child of this component, you will have to duplicate the getScore() function in that component which is harder to maintain.
Every time the page is rendered the getScore() function gets re-run once for every post in the list, and every time the user sorts the list, the calculateScore() and getScore() functions will re-run once for each item in the list, which is not very efficient.
One solution, as you alluded to, is to put these functions in the store as getters. This would solve both issues I listed above by:
Keeping your code DRY by keeping these functions in one location.
Running these functions as vuex getters will cache the results so that they only re-run if the state they are dependent on changes.
An example of what your getter property in your store may look like is:
getters: {
postsWithScore: state => {
return state.posts.map(post => {
return {
...post,
score: post.comments + post.likes,
});
}
}
See the vuex docs for more info: https://vuex.vuejs.org/guide/getters.html
I would consider moving the score calculation to a getter. It feels like a piece of the business model, why should it be fixed in a component that's merely using it? From what I recall, vuex's getter have some caching built in, so there's performance gain as well.
PS. Dynamic scoring base on time has its downsides btw. You probably don't wanna move the items around while user looks at them, but what about pagination? When user goes to the next page and your score changes in the meantime, then the continuity of the list is broke. Anyhow, it's just kinda UX concern I believe.

Using counter flag in v-for loop

I want to use a counter flag in v-for inside another v-for loop for counting total run of inside loop.
Here is my template:
<a :href="'#/product/'+list.id" :id="ikeyCounter" v-for="item,ikey in section.list" class="movie item fcosuable">
{{ ikeyCounterPlus() }}
<div class="verticalImage">
<div class="loader hideloading"></div>
<img :src="item.thumb" alt="">
</div>
</a>
data() {
return {
loading: true,
status: null,
list: [],
sections: null,
ikeyCounter: 3
}
},
And method:
ikeyCounterPlus() {
this.ikeyCounter++;
},
but I'm getting wrong result on ikeyCounter variable. Id of a tag started from "15003" to "15150", if I don't call ikeyCounterPlus() inside v-for tag, for loop will run correctly (150 run)
If you want to count your objects, then just count your data. No need to involve DOM.
section.list is an array, so section.list.length should give you desired count.
Also, as mentioned in the answer before, use some unique property of item (for example some sort of id) as the value for :key attribute.
You can't do it like this, Vue.js is reactive framework and you should learn a little bit before asking these kind of questions - https://v2.vuejs.org/v2/guide/reactivity.html
Use your key as id instead

v-for : is there a way to get the key for the nested(second) loop besides "Object.keys(obj)[0]" in Vuejs?

Here's the markup:
<ul>
<li v-for="(topic,label,index) in guides" :key="index">
<ul>
<strong> {{label}} </strong>
<li v-for="rule in topic">
{{rule.val}},
{{Object.keys(topic)[0]}}
</li>
</ul>
</li>
And here's the data for this list:
data: {
guides: {
"CSS" : {
1502983185472 : {
"modifiedby" : "bkokot",
"val" : "When adding new rule, use classes instead of ID whenever possible"
},
1502983192513 : {
"modifiedby" : "bkokot",
"val" : "Some other rule"
},
},
"Other" : {
1502628612513 : {
"modifiedby" : "dleon",
"val" : "Some test text"
},
1502982934236 : {
"modifiedby" : "bkokot",
"val" : "Another version of text"
},
}
}
}
So as you can see there is a "guides" property which is an object of other objects that do have inner objects too.
All I want is to get the keys from inner (second) loop (numbers "1502983185472" etc).
The only solution that i see right now is "Object.keys(topic)[0]", but is there a more accurate alternative in vuejs for this?
Adding key, index parameters to the second loop(with new unique variable names) doesn't work for me.
Here's a working fiddle: https://jsfiddle.net/thyla/yeuahvkc/1/
Please share your thoughts.
If there is no good solution for this - may it be a nice topic for a feature request in Vuejs repo(unless I'm missing something terrible here)?
Generally if you're curious - that number is a momentjs timestamp - I'm using this data in firebase, and saving initial timestamp as an object key seemed to be a pretty nice solution (since we need a key anyway, to save some space - I can use this key instead of another extra timestamp property in my object, this also makes the instance very 'targetable' in firebase).
Thank you in advance ! Cheers!
p.s: another possible solution is converting inner loop (css, other) from objects into arrays and using time-stamp as another object property, but I'm using firebase - saving this data as an object gives me an ability to quickly access some instance without parsing the entire parent object/array, makes it more easy to filter, search, reupdate, etc - thus converting object into array is not a good solution for instance with very large number of items.
Your fiddle renders the number key of the first entry of a topic for each of the rules in that topic. I'm assuming you want to actually show the number key for each corresponding rule.
That value is passed as the second parameter in the v-for:
<li v-for="(rule, ruleID) in topic">
{{ rule.val }},
{{ ruleID }}
</li>
Here's a working fiddle.
Here's the documentation on using v-for with an object.
This can be solved by as follows in the second loop like
<li v-for="(rule,index) in topic">
{{rule.val}},
{{index}}
</li>
Please refer this fiddle => https://jsfiddle.net/yeuahvkc/7/
Use explicit definitions
Other answers use ".val", but it's not clear where that originates.
Instead, just declare everything, like:
<li v-for="(value, key, index) in rule">
{{key}}: {{value}} - {{index}}
</li>