VueJS How can I use computed property with v-for - javascript

How can I use computed property in lists. I am using VueJS v2.0.2.
Here's the HTML:
<div id="el">
<p v-for="item in items">
<span>{{fullName}}</span>
</p>
</div>
Here's the Vue code:
var items = [
{ id:1, firstname:'John', lastname: 'Doe' },
{ id:2, firstname:'Martin', lastname: 'Bust' }
];
var vm = new Vue({
el: '#el',
data: { items: items },
computed: {
fullName: function(item) {
return item.firstname + ' ' + item.lastname;
},
},
});

You can't create a computed property for each iteration. Ideally, each of those items would be their own component so each one can have its own fullName computed property.
What you can do, if you don't want to create a user component, is use a method instead. You can move fullName right from the computed property to methods, then you can use it like:
{{ fullName(user) }}
Also, side note, if you find yourself needing to pass an arguments to a computed you likely want a method instead.

What you're missing here is that your items is an array, which holds all the items, but the computed is a single fullName, which just can't express all the fullNames in items. Try this:
var vm = new Vue({
el: '#el',
data: { items: items },
computed: {
// corrections start
fullNames: function() {
return this.items.map(function(item) {
return item.firstname + ' ' + item.lastname;
});
}
// corrections end
}
});
Then in the view:
<div id="el">
<p v-for="(item, index) in items">
<span>{{fullNames[index]}}</span>
</p>
</div>
The way Bill introduces surely works, but we can do it with computed props and I think it's a better design than method in iterations, especially when the app gets larger. Also, computed has a performance gain compared to method on some circumstances: http://vuejs.org/guide/computed.html#Computed-Caching-vs-Methods

I like the solution posted by PanJunjie潘俊杰. It gets at the heart of the issue by only iterating over this.items once (during the component creation phase), and then caching the result. This is of course the key benefit for utilizing a computed prop instead of a method.
computed: {
// corrections start
fullNames: function() {
return this.items.map(function(item) {
return item.firstname + ' ' + item.lastname;
});
}
// corrections end
}
.
<p v-for="(item, index) in items">
<span>{{fullNames[index]}}</span>
</p>
The only thing I don't like is that, in the DOM markup, fullNames is accessed by the list index. We could improve that by using .reduce() instead of .map() in the computed prop to create an Object with a key for each item.id in this.items.
Consider this example:
computed: {
fullNames() {
return this.items.reduce((acc, item) => {
acc[item.id] = `${item.firstname} ${item.lastname}`;
return acc;
}, {});
},
},
.
<p v-for="item in items" :key="`person-${item.id}`">
<span>{{ fullNames[item.id] }}</span>
</p>
Note: the above example assumes that item.id is unique, such as from typical database output.

Maybe add another v-for that iterates through a one-item-long list:
<div id="el">
<p v-for="item in items">
<template v-for="fullName in [item.firstName + ' ' + item.lastName]">
<span>{{fullName}}</span>
</template>
</p>
</div>
Not nice, but that's what you're looking for: an object around that span that has a property called fullName that contains that specific value.
And it's not just a vanity feature, because we may need to use that value at more than one place, eg.:
<span v-if="...">I am {{fullName}}</span>
<span v-else-if="...">You are {{fullName}}</span>
<span v-else>Who is {{fullName}}?</span>
My use case was that I was constructing dates in v-for loops (yes, another calendar), like:
<v-row v-for="r in 5">
<v-col v-for="c in 7">
<template v-for="day in [new Date(start.getTime() + 24*60*60*1000*((c-1) + 7*(r-1)))]">
<span>
Some rendering of a day like {{day.getYear()}} and
{{day.getMonth()}} etc.
</span>
</template>
</v-col>
</v-row>
(For brevity I omitted the :key="whatever" settings)
I admit that the nicest way would be to move that to a separate component, but if we create a new component for every two-liner like this, and use that component only at this single place, then we just pollute another namespace.
Maybe a v-let="day as new Date(...)" directive would be handy for such purpose.

you have to pass argument in the function call
<div id="el">
<p v-for="item in items">
<span>{{fullName(item)}}</span>
</p>
</div>

Related

Handlebar Each and JSON API call how do I get it to transfer the data from array response and put it on a handlebars modal

$("#searchMovieBtn").click(() => {
const movieSource = $("#movie-template").html();
const movieList = Handlebars.compile(movieSource);
const movieSearch = $("#addMovie").val();
console.log(movieSearch);
queryURL = `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&language=en-US&query=${movieSearch}&page=1&include_adult=false`;
$.ajax({
url: queryURL,
method: "GET"
}).then(response => {
console.log(response);
const results = response.results;
const data = { movies: [] };
for (let i = 0; i < results.length; i++) {
const currentTitle = {
title: results[i].title,
release_date: results[i].release_date
};
data.movies.push(currentTitle);
}
console.log(data);
$("#placeholder").html(
movieList({
data: data
})
);
});
});
});
this is my function to call it
<div id="placeholder"></div>
<script type="text/x-handlebars-template" id="movie-template">
<ul class="list-group">
{{#each movies}}
{{#each this}}
<li class="list-group-item text-dark" id="MovieTitle">{{title}}
<span>
(<span id="MovieYear">{{release_date}}</span>)
<span>
<button class="btn btn-warning text-light btn-sm" id="addMovieBtn">Add to watchlist</button>
</span>
</span>
</li>
{{/each}}
{{/each}}
</ul>
</script>
And what it is going into!
I have been up all night and just cannot figure out what is going on with it! I swear I have looked up and down and still cannot find the right method to use it is driving me mad! Anyways thanks for the help ahead of time I am sure it was something very very simple that I looked over a lot.
There are a few errors in your solution.
First, you must understand the context object that you are passing to your template. It's shape looks like this:
{
data: {
movies: [
{
title: "Movie Title",
release_date: "Year"
}
]
}
}
Notice that the first key in the object is data. However, in your template, you have no reference to data, but try to access movies directly, effectively skipping a reference. Given this data, the first #each in your template would need to be updated to:
{{#each data.movies}}
Your second problem is your second #each in your template. This #each iterates over each property of each movie in the movies array. So when you reference a property like {{title}} within this block, the full path of your reference is something like data.movies[i].title.title. There is an extra '.title' there because you are not within the context of the movie, but of one of its properties (title in this case).
To fix this, simply remove the second #each.
I have created a fiddle for your reference.
As an additional note: I would avoid including static element IDs inside of a loop. It means you will have duplicate IDs in your output and that is not valid HTML.

Proper way of object copy from one element to another in Vue.js

I am new to Vue.js (I mostly use PHP) and I am trying to creating simple view where user can add an object from one component and place it's copy into another component.
Main template
<template>
<div class="left">
<TaskList :tasks="tasks" v-on:pinned-add-task="pinnedAddTask" />
</div>
<div class="right">
<PinnedList :pinned="pinned" />
</div>
</template>
TaskList
<template>
<div class="task-list">
<div v-for="task in tasks" :key="task.id">
<TaskItem :task="task" v-on:pinned-add-task="$emit('pinned-add-task', task)" />
</div>
</div>
</template>
TaskItem
<template>
<div>
<p>{{task.name}}</p>
<button v-on:click="$emit('pinned-add-task', task)">+</button>
</div>
</template>
And as far as I am aware object "task" is passed by reference and when I try to create an empty object or an array and insert "task" into that newly created object/array when I change original "task" it is also being changed inside that new object and I don't want that.
I am getting my data (tasks) from API that I have created and I am using pagination system so I want to be able to switch pages without losing it from the pinned page.
I created code which looks like this but I don't like it and I don't think that's a good way to do this:
pinnedAddTask(item) {
let pQuantity = 1; // I use this value because I want to be able to pin one task multipletimes
let left = this.pinned;
let right = [];
for (let task of this.pinned) {
if(item.id == task.id) {
pQuantity = task.quantity + 1;
left = this.pinned.filter(eItem => eItem.id < item.id);
right = this.pinned.filter(eItem => eItem.id > item.id);
}
}
const clone = {...item, quantity: pQuantity};
this.pinned = [...left, clone, ...right];
}
Can anyone confirm or reject this?
Yes this one is fine if thats just a shallow copy [ level-one object].
But if you are having a nested object then you might have to use recursive methodology or use any external libary like lodash

How to define a temporary variable in Vue.js template

Here is my current template:
<a-droppable v-for="n in curSize" :key="n - 1" :style="{width: `${99.99 / rowLenMap[orderList[n - 1]]}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLenMap[orderList[n - 1]] > 10}">
<some-inner-element>{{rowLenMap[orderList[n - 1]]}}</some-inner-element>
</a-draggable>
</a-droppable>
The problem is that i have to write rowLenMap[orderList[n - 1]] multiple times, and i'm afraid vue.js engine will also calculate it multiple times.
What i want is something like this:
<a-droppable v-for="n in curSize" :key="n - 1" v-define="rowLenMap[orderList[n - 1]] as rowLen" :style="{width: `${99.99 / rowLen}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLen > 10}">
<some-inner-element>{{rowLen}}</some-inner-element>
</a-draggable>
</a-droppable>
I think it's not difficult to implement technically because it can be clumsily solved by using something like v-for="rowLen in [rowLenMap[orderList[n - 1]]]". So is there any concise and official solution?
I found a very simple (almost magical) way to achieve that,
All it does is define an inline (local) variable with the value you want to use multiple times:
<li v-for="id in users" :key="id" :set="user = getUser(id)">
<img :src="user.avatar" />
{{ user.name }}
{{ user.homepage }}
</li>
Note : set is not a special prop in Vuejs, it's just used as a placeholder for our variable definition.
Source: https://dev.to/pbastowski/comment/7fc9
CodePen: https://codepen.io/mmghv/pen/dBqGjM
Update : Based on comments from #vir us
This doesn't work with events, for example #click="showUser(user)" will not pass the correct user, rather it will always be the last evaluated user, that's because the user temp variable will get re-used and replaced on every circle of the loop.
So this solution is only perfect for template rendering because if component needs re-render, it will re-evaluate the variable again.
But if you really need to use it with events (although not advisable), you need to define an outer array to hold multiple variables at the same time :
<ul :set="tmpUsers = []">
<li v-for="(id, i) in users" :key="id" :set="tmpUsers[i] = getUser(id)" #click="showUser(tmpUsers[i])">
<img :src="tmpUsers[i].avatar" />
{{ tmpUsers[i].name }}
{{ tmpUsers[i].homepage }}
</li>
</ul>
https://codepen.io/mmghv/pen/zYvbPKv
credits : #vir us
Although it doesn't make sense here to basically duplicate the users array, this could be handy in other situations where you need to call expensive functions to get the data, but I would argue you're better off using computed property to build the array then.
Judging by your template, you're probably best off with a computed property, as suggested in the accepted answer.
However, since the question title is a bit broader (and comes up pretty high on Google for "variables in Vue templates"), I'll try to provide a more generic answer.
Especially if you don't need every item of an array transformed, a computed property can be kind of a waste. A child component may also be overkill, in particular if it's really small (which would make it 20% template, 20% logic and 60% props definition boilerplate).
A pretty straightforward approach I like to use is a small helper component (let's call it <Pass>):
const Pass = {
render() {
return this.$scopedSlots.default(this.$attrs)
}
}
Now we can write your component like this:
<Pass v-for="n in curSize" :key="n - 1" :rowLen="rowLenMap[orderList[n - 1]]" v-slot="{ rowLen }">
<a-droppable :style="{width: `${99.99 / rowLen}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLen > 10}">
<some-inner-element>{{rowLen}}</some-inner-element>
</a-draggable>
</a-droppable>
</Pass>
<Pass> works by creating a scoped slot. Read more about scoped slots on the Vue.js documentation or about the approach above in the dev.to article I wrote on the topic.
Appendix: Vue 3
Vue 3 has a slightly different approach to slots. First, the <Pass> component source code needs to be adjusted like this:
const Pass = {
render() {
return this.$slots.default(this.$attrs)
}
}
Today I needed this and used <template> tag and v-for like this
I took this code and
<ul>
<li v-for="key in keys"
v-if="complexComputation(key) && complexComputation(key).isAuthorized">
{{complexComputation(key).name}}
</li>
</ul>
Changed it to this
<ul>
<template v-for="key in keys">
<li v-for="complexObject in [complexComputation(key)]"
v-if="complexObject && complexObject.isAuthorized">
{{complexObject.name}}
</li>
</template>
</ul>
And it worked and I was pleasantly surprised because I didn't know this was possible
This seems like the perfect use case of a child component. You can simply pass your complex computed value(s) as a property to the component.
https://v2.vuejs.org/v2/guide/components.html#Passing-Data-to-Child-Components-with-Props
How about this:
<div id="app">
<div
v-for="( id, index, user=getUser(id) ) in users"
:key="id"
>
{{ user.name }}, {{ user.age }} years old
<span #click="show(user)">| Click to Show {{user.name}} |</span>
</div>
</div>
CodePen: https://codepen.io/Vladimir-Miloevi/pen/xxJZKKx
<template>
<div>
<div v-for="item in itemsList" :key="item.id">
{{ item.name }}
<input v-model="item.description" type="text" />
<button type="button" #click="exampleClick(item.id, item.description)">
Click
</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{
id: 1,
name: 'Name1',
},
{
id: 2,
name: 'Name2',
},
],
}
},
computed: {
itemsList() {
return this.items.map((item) => {
return Object.assign(item, { description: '' })
})
},
},
methods: {
exampleClick(id, description) {
alert(JSON.stringify({ id, description }))
},
},
}
</script>
Just tested using vue3 and works, i think it works universally
{{ (somevariable = 'asdf', null) }}
<span v-if="somevariable=='asdf'">Yey</span>
<span v-else>Ney</span>
It outputs nothing while setting your variable.
mandatory:
opening "("
set your variable
closing ", null)"
curSize is an array. Your temporary values comprise a corresponding implied array sizedOrderList = curSize.map(n => orderList[n-1]). If you define that as a computed, your HTML becomes
<a-droppable v-for="n, index in sizedOrderList" :key="curSize[index]" :style="{width: `${99.99 / rowLenMap[n]}%`, order: n}">
<a-draggable :class="{thin: rowLenMap[n] > 10}">
<some-inner-element>{{rowLenMap[n]}}</some-inner-element>
</a-draggable>
</a-droppable>

VueJS v-for loop breaking when array values are equal to each other

my pen: http://codepen.io/leetzorr/pen/aprZqO
html
<template v-for="spot in bars" :key="item.$index">
<div id="bar-holder">
<div class="bars">
<ul>
<span>{{ $index }}</span>
<li v-for="n in bars[$index]"></li>
</ul>
<button v-on:click="increase($index)">+</button>
</div>
</div>
</template>
javascript
var par = {
bars : [ 1, 5, 6 ]
}
var cl = new Vue({
el: '#container',
data: par,
methods: {
increase: function (index) {
var value = this.bars[index];
value++;
par.bars.$set(index, value);
},
}
})
So whenever you click the increase button under each group of bars, that value in the par.bars array increases. For some reason, whenever par.bar[index]'s value equals that of one of its siblings, one of the bar elements disappears.
I've went over my code for about an hour now, and cannot figure out where this is breaking.
Replace
<template v-for="spot in bars" :key="spot.$index">
with:
<template v-for="spot in bars" :key="spot.$index" track-by="$index">
Explanation in the Vue.js guide: http://v1.vuejs.org/guide/list.html#track-by-index
This is because Vue reuse templates with same key.
To avoid that you could use index as key (apparently that was you were trying to do in first place!)
Change your template's directive v-for to something like this
<template v-for="spot in bars.length">
Please see this working fiddle

VueJs: How to Edit an Array Item

Simple Todo-App. Please excuse my ignorance for making a rather basic question.
But how would you go about and edit a certain item on an array?
Normally I would try to bind the value of my input to a new property on my data object and then assign this new property to the old property on click throuch Vue's two way databinding.
Like this: http://jsfiddle.net/z7960up7/
Well in my case I use the v-repeat directive, which loops through my data array but I can't use the v-model directive to use the two way databinding, because the values of the properties get corrupted if I do so. (See here: http://jsfiddle.net/doL46etq/2/)
And now I wonder, how I would go about updating my array of tasks:
My idea is to pass the VueObject (this) through my method on click, and then define the index on my event handler and then updating the tasks array, using the index, like this:
HTML:
<input v-el="editInputField" type="text" value="{{ task.body }}" v-on="keyup: doneEdit(this) | key 'enter'"/>
<button v-on="click: editTask(this)">
Edit
</button>
JS:
methods: {
editTask: function (task) {
var taskIndex = this.tasks.indexOf(task.task);
this.tasks[taskIndex] = {
'body': document.querySelector('input').value,
'completed': false
};
console.log(task.task.body);
},
}
Here is my fiddle about it:
http://jsfiddle.net/doL46etq/3/
But the data object is not updated at all and I wonder how I would go about it and update it.
What is the best way to edit an element on the array, using Vue?
Edit: An easy way, would just be to delete the element, and add the new to the array, using the push method, but I really want just to update the element, because I like to keep the dataobject in sync with my backend.
The short answer: Use a component in an extended constructor, then pass the index to that component in HTML as property and use computed properties to link back and forth to your data.
But don't be satisfied with just the short answer. Here is the long one:
Situation: I am using your JSFiddle as base for this answer.
in HTML you have:
<td>{{ task.body }}</td>
<td>
<div>
<input v-el="editInputField" type="text" value="{{ task.body }}" v-on="keyup: doneEdit(this) | key 'enter'" v-model="newEdit"/>
</div>
</td>
<td>
<button v-on="click: editTask(this)" class="mdl-button mdl-js-button mdl-button--icon"> <i class="material-icons">create</i>
</button>
</td>
We want to replace this code with the component. Using this component allows us to identify the index/row we are working on in your set of data.
<td v-component="listitem" index="{{$index}}"></td>
Next step: defining the component.
In order not to cloud our instance with the component, we will create a separate constructor for the vue object, so we can assign the new element to our new object.
This is done using extend.
window.newVue = Vue.extend({
components:
{
'listitem': {
props: ['index'],
computed: {
// both get and set
body: {
get: function () {
return this.$parent.tasks[this.index].body;
},
set: function (v) {
this.$parent.tasks[this.index].body = v
}
}
},
template: '<td>{{ body }}</td><td><div><input type="text" v-model="body" value="{{ body }}"/></div></td><td></td>',
}
}
});
Since we can't define our data properly using an extend, we'll just assume the data already exists while writing the component.
Step 3: defining the actual data:
Instead of using Vue as our object constructor, we'll now use our newly created instantiator.
new newVue({
el: '#todoapp',
data: {
tasks: [{
'body': 'Eat breakfast',
'completed': false
}, {
'body': 'Drink milk',
'completed': false
}, {
'body': 'Go to the store',
'completed': false
}],
newTask: '',
},
});
That's it!
In case you couldn't follow what happened: Here's the Fiddle!
PS: More information about the working of these code can be found on vuejs.org
Actually the simplest way to update an array item, is to two-way bind the task body with the v-model directive.
Example:
http://jsfiddle.net/z7960up7/2/
<div id="demo">
{{ message }}
<div class="edit">
<input type="text" v-model="message">
<button v-on="click: editMessage">Edit</button>
</div>
<pre>{{ $data | json }}</pre>
</div>
And fire an event whenever you blur out of the input box or the edit button is hit.
Also hide the input field with css, by using the v-class directive.

Categories