Using computed value in v-for (Vue.js) - vue.js

I'm currently making bitcoin trading web app (personal project. Not a business one)
I have a bunch of cryptocurrencies prices from API, and showing it all by using v-for loop.
here's part of my code:
<tbody class="text-sm">
<tr v-for="coin in props.coins" :key="coin.code" class="hover:bg-zinc-600">
<td class="p-1">
<div class="font-semibold">{{ coin.name }}</div>
<div class="text-xs text-gray-400">{{ coin.code }}</div>
</td>
<td
class="text-right font-bold align-top p-1"
>{{ Number(coin.trade_price).toLocaleString() }}</td>
<td
:class="{ 'text-blue-400': isNegative(coin.signed_change_rate), 'text-red-400': isPositive(coin.signed_change_rate) }"
class="text-right p-1"
>
<div>{{ Number(coin.signed_change_rate).toFixed(2) }}%</div>
<div class="text-xs">{{ coin.signed_change_price }}</div>
</td>
<td class="text-right align-top p-1">{{ convertTp24h(coin.acc_trade_price_24h) }}</td>
<td class="text-right p-1">
<button class="bg-red-700 rounded-lg hover:bg-red-600 p-1">매수</button>
</td>
</tr>
</tbody>
As you can see I have many methods and expressions that converts raw value from API to human-readables.
I'm wondering if this is a bad practice. I heard that methods are called everytime so, rather use computed().
But as you can see I have my values from API in Object(props.coins) and I'm looping this object in v-for. Can I still use computed() methods to convert a value inside an Object that is also looped by v-for?

You can't use computed for items in a loop, since they don't take in arguments. Computed is used for .. computed properties and methods take in arguments e.g. formatChangeRate()
You are right that methods are called everytime, but that's totally fine and normal practice.
So creating a component with methods like formatChangeRate, formatTradePrice is totally fine.
There are ways to make it work with computed properties but in my opinion its not worth it.
1)
You coould make another component that takes in the item as a prop, uses computed property and displays it in componenent's template but that's a total overhead.
2)
You could map the array and reference it by index. Something like:
computed: {
changeRate() {
this.coins.map(coin => {
return Number(coin.signed_change_rate).toFixed(2) + '%'
})
}
}
Now changeRate is an array of formatted change rates so you could in your v-for do something like
v-for="(coin, index) in coins)">
<td>changeRate[index]</td>
So now you're looping through the array multiple times, and the code is less testable and less readable.
In my opinion using methods is totally fine and the right way to go in this example. The performance of calling a simple formatting method multiple times is negligible.
Also the cache you're refering to is that computed properties are cached based on their reactive dependency. E.g. you have in your data, firstName and lastName. You could then make a computed property called fullName that concats those two. The caching would work so that fullName doesn't change unless firstName or lastName changes.
Since you're looping through an array the value that would be computed is always changing.

you are right, methods are called everytime and in theory thats not really good, but in practice if your method just concat strings or other simple operations, that's really not a big deal. Your computer can do millions of operations like that per seconds.
In the other hand if you do complex operations then the solution is to precalculate before.
After you receive your data construct an Array looking something like bellow and use this one in your template
(pseudo code)
[{
name,
code,
changeRate: Number(signed_change_rate).toFixed(2),
...
},
...
]

Related

How to use `v-if` and `v-for` on the same element?

Hi I'm trying to figure out how to use v-if on a iterated element which also uses v-for. I need to check if the current element has any of a series of classes, which are numbers.
so the classes of each article would be:
<article class="post-item post-guide 12 22 19 30 55">...
this is the HTML that renders all:
<article v-if="showIfHasClass" :class="'post-item post-guide ' + guide.categories.toString().replace(/,/g, ' ')"
v-for="(guide, index) in guides" :key="index">
<header>
<h1 class="post-title">
{{ guide.title.rendered}}
</h1>
</header>
</article>
I have tried with methods that check the class of all elements, that works, but i'm trying to use a clean Vue built-in solution with v-if without success, i'm not able to retrieve the class of this in a successful way.
Should showIfHasClass be a computed property? I have tried with that too... but it seems, I'm missing something along the way.
my data I have to check against is an array:
data:{
guides: [...]
selectedCategories: [1, 22, 33, 100, 30];
}
or maybe it is better to directly loop over the guides and check if they have the selectedCategory or not, then remove the element from the guides data array?
What is more effective?
Besides the option to create an additional filtered computed (effectively eliminating the need to use v-for and v-if on the same element), you also have a template level way of dealing with such edge-cases: the <template> tag.
The <template> tag allows you to use arbitrary template logic without actually rendering an extra element. Just remember that, because it doesn't render any element, you have to place the keys from the v-for on the actual elements, like this:
<template v-for="(guide, index) in guides">
<article v-if="isGuideVisible(guide)"
:key="index"
class="post-item post-guide"
:class="[guide.categories.toString().replace(/,/g, ' ')]">
<header>
<h1 v-text="guide.title.rendered" />
</header>
</article>
</template>
isGuideVisible should be a method returning whether the item is rendered, so you don't have to write that logic inside your markup. One advantage of this method is that you can follow your v-if element with a fallback v-else element, should you want to replace the missing items with fallback content. Just remember to also :key="index" the fallback element as well.
Apart from the above use-case, <template> tags come in handy when rendering additional wrapper elements is not an option (would result in invalid HTML markup) (i.e: table > tr > td relations or ol/ul > li relations).
It's mentioned here as "invisible wrapper", but it doesn't have a dedicated section in the docs.
Side note: since you haven't actually shown what's inside guide.categories, I can't advise on it, but there's probably a cleaner way to deal with it than .toString().replace(). If guide.categories is an array of strings, you could simply go: :class="guide.categories".
I think the most Vue way is to create a computed property with filtered items from selected categories, then use that in v-for loop (the idea is to move the business logic away from template).
computed: {
filteredItems(){
return this.guides.filter(e => this.selectedCategories.includes(e.category))
}
}
Also, as a note, it is not recommended to use v-if and v-for on the same element, as it may interfere with the rendering and ordering of loop elements. If you don't want to add another level of nesting, you can loop on a 'template' element.
<template v-for="item in items">
// Note the key is defined on real element, and not on template
<item-element v-if='condition' :key="item.key"></item-element>
</template>

vue application best practice/pattern to display data with people-friendly label?

A high-level vue application question:
A little background:
small team, still learning vue
creating a new large reasonably complex vue app from scratch
vue-cli scaffolded (vue 2, veux, router, i118n, bootstrapVue)
there are many different application entities – e.g. "user", “product”, etc.
there are many of each item, and each has a data record (from a db) that includes a number of boolean fields such as "active", “enabled”, “is_sensitive”.
item data displays in many different contexts, including: search results, table lists (e.g. browse), individual landing views for each (an item detail page), as well as ancillary lists (e.g. “A related item:”
The key problem:
In every display situation, the boolean values should to be translated from machine-friendly to people-friendly terminology.
The appropriate human-friendly term depends on the field itself (so there is no single translation for all fields).
Example:
Machine-friendly data:
[
{
name: “Megaphone”,
is_embarrassing: false,
active: false,
},
{
name: “Wankel Rotary Engine”,
is_embarrassing: true,
active: true,
},
]
Human-friendly list:
+----------------------+----------+---------------------+
| Name | Active? | Embarrassing? |
+----------------------+----------+---------------------+
| Megaphone | Inactive | Not embarassing |
| Wankel Rotary Engine | Active | Item is Embarassing |
+----------------------+----------+---------------------+
The key question:
What is the best way to solve this in a scalable, efficient, elegant, sensible way?
I have thought of a couple of options…neither of these feel scalable nor elegant,
and are brittle.
(1) sequence of in-line v-if conditions within the component view template
<p v-if=“item.property.is_embarrassing">
Item is Embarrassing
</p>
<p v-else>
Not embarassing
</p>
(2) computed properties in the component
<p>
{{ detailsPropertyEmbarrassing }}
</p>
detailsPropertyEmbarrassing() {
return item.property.is_embarrassing ? “Item is Embarrassing : “Not Embarrassing”;
},
I have also been noodling over the idea of some sort of Map that is imported along with the data and used to get the right labels, but I haven’t completely worked that out yet.
What is a solution for transforming data fields to people-friendly labels across an entire application, for a variety of different display situations?
(Side note: I may also need to transform the field in other ways, such as truncating length…)
And is there a way to establish this globally in the app in a manner that is scalable, both technically and organizationally,
so that new components can display as desired, near-automatically?
This seems like a basic fundamental need in any app of size, not just vue,
so I feel like this has been/has to have been solved before, but either I cannot find the right research keywords or am missing something obvious.
Thanks in advance!
You can think of the VueComponent as a ViewModel (or Controller if you like MVC). It's indented purpose is to fetch information from the model (backend) and transform it to something that your view can use.
Thus, the change from a boolean to a string should be done in the VueComponent and not in the view.
In your view component, declare something like myItems = []. When you fetch it, traverse the result:
backendResult.forEach(item => {
this.myItems.push({
name: item.Name,
embarrassing: item.IsEmbarrassing ? "Oh yes!" : "No"
});
Then the view is simplified:
<table>
<tbody>
<tr v-for="item in myItems">
<td>{{item.Name}}</td>
<td>{{item.embarrassing}}</td>
</tr>
</tbody>
</table>
Another solution is to use filters which requires no modification of the vue component and just a small change of the view:
<table>
<tbody>
<tr v-for="item in myItems">
<td>{{item.Name}}</td>
<td>{{item.isEmbarrassing|embarrassing}}</td>
</tr>
</tbody>
</table>
Notice the |embarrassing part in the view. It uses a filter defined as:
Vue.filter('filter', function (value) {
if (!value) return 'not embarrassing'
return 'embarrassing';
})

Aurelia Possible Memory Leak

I have a table template that is leaking I have pinpointed the code causing the issue to a single line.
<tbody>
<tr repeat.for="row of workListData.rows" click.delegate='resultItemClick($event, row)'>
<td repeat.for="col of workListData.columns">
<template if.bind='col.name === "isChecked"'>
${col.label}
</template>
<template if.bind='col.name !== "isChecked"'>
**${row.Properties[col.name]}**
</template>
</td>
</tr>
</tbody>
${row.Properties[col.name]}
This is the line of code causing the issue this line takes the current row and uses the column name to access the value of the column
enter image description hereOk it seems aurelia is leaking when I have nested repeat.for
Leaks
<tr repeat.for="row of workListData.rows" click.delegate='resultItemClick($event, row)'>
<td repeat.for="col of workListData.columns">
</td>
</tr>
Does not leak
<tr repeat.for="row of workListData.rows" click.delegate='resultItemClick($event, row)'>
</tr>
Its definitely coming from nested repeat problem is if I simplify the code IE remove web services etc its work as expected
One of my colleagues has noticed something interesting commenting out a different template controller also stops the leak seems its not fussy which controller gets commented out.
Finally located the leak it seems I had a couple however one thing for sure is aurelia seems to have a very bad tendency to leak if you do this in a template
${row.Properties[key]}
Not sure if that is supported though
I now instead pass the object and key to a function and return the value which solves the issue

Looping over two-dimensional array containing objects in Vue.JS

The problem
In Vue, I'm passing an array called issues. The array contains (at present) two objects, but can contain infinite amounts of objects. Every object then has another array named issues, nested inside of it.
The issue is that when I need to display the data, I find that I can't seem to reach the inner "issues" section of it.
I can loop through the first array like so:
<tr v-for="issue in issues" track-by="id">
But that only lets me see the first two objects. I then tried:
<tr v-for="issue in issues" track-by="id">
<td>
<div class="btn-table-align" v-for="issue_title in issue.issues">
#{{ issue_title.title }}
</div>
</td>
</tr>
Which lets me access the sub-elements, but doesn't generate enough rows. I then tried looping over it AGAIN, like so:
<div v-for="first in issues" track-by="id">
<tr v-for="issue in first" track-by="id">
<td>
<div class="btn-table-align">
#{{ issue.id }}
</div>
</td>
</tr>
</div>
But, alas - it generates no rows at all when I do that.
I'd basically need a way to run a "issue in issues", then another for the results and THEIR direct children. The only issue is - I can't figure out how to do it, and Vue won't respond to any of the above attempts! I find a severe lack of documentation on two-dimensional arrays in Vue as well, which has me confused further.
Can anyone shed some light on this? Is it possible, or do I need to adjust the data sent to Vue differently?
To help, I shot an image of an example structure: http://i.imgur.com/6Oz67R9.png
This was a typical 5am question, where I now realize that the data I'm passing makes no sense - it should be the other way around. The actual issues should be in the first array, and the subarray should contain affected servers.

Variables in Handlebars

I'm passing models to a handlebars template and putting a few of the properties in a row. I want to have some sort of position variable for each row. Specifically, I want to know how many rows have been created and have a position associated with each row.
e.g.
{{#each row}}
<span class = (indexHere)>
<tr>
<td> <{{this.property}}>
</tr>
</span>
{{/each}}
This is an old question and maybe at that time Handlebars did not include the #index property, but according to current (1.3.0) documentation:
When looping through items in each, you can optionally reference the current loop index via {{#index}}
You make your own helper :-)
Here's one you can steal:
https://gist.github.com/1048968