Trying to figure out why the following code calls someFunction() 4 times instead of 1 time:
html:
<div id="app">
<div v-for="item in items" :key="item.id">
<input v-model="item.test">
<div>{{ someFunction(item) }}</div>
</div>
</div>
vue:
new Vue({
el: '#app',
data: {
items: [
{
id: 0,
test: 'test1',
},
{
id: 1,
test: 'testSomething',
},
{
id: 2,
test: 'foo',
},
{
id: 3,
test: 'bar',
},
]
},
methods: {
someFunction(item) {
console.log(item.test);
return item.test;
},
}
})
You can play with the code here
So my real world application has 30 items and a more complex someFunction(). At the moment, someFunction() is called once for every item as soon as only one item changes. But why is vue calling the function n times instead of only the one time needed?
EDIT: My problem are NOT the n function calls when the page is loaded. My problem are the n function calls when the input of only one input field is changed and thus only one function call is necessary, because all the other input values remain the same and thus the function result remains the same.
By using v-model on nested item in "items" you are invoking global change on "items" variable, and by that the whole html is re-rendered because there was sensed change.
You want this re-render because v-model made some changes you want to keep displaying updated data.
<div id="app">
<div v-for="item in items" :key="item.id"> <------- 1.LOOP here
<input v-model="item.test">
<div>{{ someFunction(item) }}</div> <--------2.FUNCTION call here
</div>
</div>
that means you go through each item in items array.
each iteration calls the method someFunction(item)
if you want a conditional executing you should <div v-if="onlyIfIwant">{{ someFunction(item) }}</div> wrap it into a v-if to prevent the function call
update
if you want conditional executes of the someFunction() then of course you have to add a flag in your objects like this:
data: {
items: [
{
id: 0,
test: 'test1',
execute: true
},
{
id: 1,
test: 'testSomething',
execute: false
},
{
id: 2,
test: 'foo',
execute: false
},
{
id: 3,
test: 'bar',
execute: true
},
]
},
your v-if just needs the following:
<div v-if="item.execute">{{ someFunction(item) }}</div>
Related
In my components template I have following code:
<div
class="program-point-container"
v-for="p in programPoints"
:key="p.id"
>
<div class="mt-6 mt-sm-3 mt-md-14 mb-3 font-weight-bold font-size-16" v-if="showDate(p.startsAt)">
{{new Date(p.startsAt) | moment('DD.MM.YYYY')}} // Humboldt Carré
</div>
</div>
My programPoints Array has 23 object´s in it. For debug reasons I console logged a string to see that the function is called 23 * 2 times.
In this method I push data into an array. And return true or false to show the div container. But since it already pushed and returned at the "first" rendering. I don't get to show that container.
showDate(datetime) {
const date = new Date(datetime).toDateString()
// check if date already been pushed
if(this.dates.includes(date)) {
console.log('false')
return false // date already been pushed
}
this.dates.push(date)
console.log('true')
return true
},
Throw away "JQuery thinking". This is NOT how Vue works. Your template is and will be rendered multiple times - simply every time some of the reactive data it uses changes...
You don't need to care about what was rendered before and what to do to update the result for a new data state. It is Vue's job. Just write your template in a "what I want to see on the screen" style and Vue will take care of the rest...
Just remove everything related to showDate - it's not needed. And read the documentation!
Update
In the Array of Objects I have a property which has datetimes. And I only want to display the date once on the first object the date appears. And ignore it for the rest. When a new date appears I want it to show it too but only for the first object and so on
I want to add an extra key to the Object which is the first to iterate and has that unique date. The others will be ignored, till another one comes with an unique date.
First rule of Fight Club...ehm Vue is: Never modify any data which are used in template during template rendering. In best case you end up with incorrect result (as you did). In worst case you end up with infinite loop...
Much better solution is to create alternative data structure that represents what you want and thanks to Vue computed properties it stays up to date even if original data changes...
const vm = new Vue({
el: "#app",
data() {
return {
programPoints: [
{ id: 1, name: 'Program A', startsAt: '2021/04/13' },
{ id: 2, name: 'Program B', startsAt: '2021/04/14' },
{ id: 3, name: 'Program C', startsAt: '2021/04/14' },
{ id: 4, name: 'Program D', startsAt: '2021/04/18' },
{ id: 5, name: 'Program E', startsAt: '2021/04/13' },
{ id: 6, name: 'Program F', startsAt: '2021/04/18' },
]
}
},
computed: {
programByDays() {
return this.programPoints.reduce((acc, item) => {
if(!acc[item.startsAt]) {
acc[item.startsAt] = []
}
acc[item.startsAt].push(item)
return acc
}, {})
}
},
mounted() {
setTimeout(() => this.programPoints.push({ id: 7, name: 'Program SPECIAL', startsAt: '2021/04/20' }), 2000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul>
<li v-for="(programs, date) in programByDays" :key="date">
<p> {{ date }} </p>
<ul>
<li v-for="program in programs" :key="program.id">
{{ program.name }}
</li>
</ul
</li>
</ul>
</div>
Given a collection of objects in the data of a component:
data: function () {
return [
{ id: 1, name: "foo", br: false },
{ id: 1, name: "bar", br: true },
{ id: 1, name: "baz", br: false }
]
}
...is it possible to render a structure like so...
<div id="1">foo</div>
<div id="2">bar</div><div class="break" />
<div id="3">baz</div>
In a nutshell, I need to have another div conditionally rendered at the same level as the items in the list. If it matters or helps, the individual items in the list are also components. I know how to set up the rest of the data and properties - it's just getting that additional HTML rendered in the list that I need to accomplish.
I want to avoid creating another item in the list and additional component to represent the break. No need to add the overhead of the additional Vue objects for the simple HTML div. This list may have > 100 items and "breaks" and it can add up quickly.
Yes. You should loop through the items like so:
<template v-for="item in items">
<div :id="item.id">
{{ item.name }}
</div>
<div class="break" v-if="item.br">
</div>
</template>
You can do it with a normal v-for and a normal v-if for your optional div
<html>
<head>
<script type = "text/javascript" src = "https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js">
</script>
</head>
<body>
<div id="app">
<div v-for="item in items">
<div :id="item.id">{{item.name}}</div>
<div v-if="item.br" class="break">
</div>
</div>
<script type = "text/javascript">
var vue_det = new Vue({
el: '#app',
data: {
items: [
{ id: 1, name: "foo", br: false },
{ id: 2, name: "bar", br: true },
{ id: 3, name: "baz", br: false }
]}
});
</script>
</body>
</html>
You should not be afraid of 100 divs or around so, a library like Vue is made to manage efficiently thousands of components
I have several accordions (every one is a single Vue component) and they are expanded by default. There's also a 'copy' function allowing to make a duplicate of every component.
Vue.component("Accordion", {
template: "#accordion-template",
data: function () {
return {
open: true
}
},
methods: {
toggle: function () {
this.open = !this.open;
}
}
});
new Vue({
el: '#vue-root',
data: {
devices: [
{
name: "a", description: "first"
},
{
name: "b", description: "second"
},
{
name: "c", description: "third"
}
]
},
methods: {
copy: function (device) {
var index = this.devices.indexOf(device) + 1;
var copy = {
name: device.name + "_copy",
description: device.description + "_copy"
};
this.devices.splice(index, 0, copy);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.js"></script>
<div id="vue-root">
<div class="device" v-for="device in devices">
<accordion>
<div slot="acc-head">
<span>{{ device.name }}</span><br/>
<button #click="copy(device)">copy</button>
</div>
<div slot="acc-body">
{{ device.description }}
</div>
</accordion>
</div>
</div>
<script type="text/x-template" id="accordion-template">
<div>
<slot name="acc-head"></slot>
<button #click="toggle">Open: {{ open }}</button>
<div :class="open ? 'active' : 'hidden'">
<slot name="acc-body"></slot>
</div>
</div>
</script>
When all accordions are collapsed (in other words 'open: false') and I try to duplicate an accordion from the middle of list (for example b), I expect appearing of the new component named 'name'_copy and it must be expanded by default. But instead of this, the new component has the same values of all attributes as the duplicated one and the last component in the list becomes expanded.
How can I solve this issue?
Fiddle: https://jsfiddle.net/j3ydt1m7/
Short answer
Add a key in your v-for loop: v-for="device in devices" :key="{something here}". Your key must be unique and identify each device, even after device copy
Code
Please check: https://jsfiddle.net/Al_un/9cradxvp/. For debugging purpose, I changed few things:
I put device as props of <accordion> so that I can use device properties in console.log
Copying device is now emitted from <accordion>. Vue doc on listening to child component events
I have added mounted() and updated() hooks. More about Lifecycle hooks
Each device has an ID
Long answer
About list rendering
If key is not provided in v-for loop, Vue does not know how to update a List. From Vue documentation:
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item.
Let's consider your list (I have added one element)
[
{id: 1, name: "a"},
{id: 2, name: "b"},
{id: 3, name: "c"},
{id: 4, name: "d"},
]
Now, let's copy node "b". Without :key="device.id", the console output is
4: d is mounted
3: c is updated
5: b_copy is updated
With :key="device.id", the console output is only:
5: b_copy is mounted
Basically, without keys, there are:
two updates: c becomes b_copy, d becomes c
one insert: d is created
Consequently, the last element is recreated every time you proceed to a copy. As open default value is true, obviously, d has open = true.
If each element has a :key="device.id", then only element b_copy is created
To check that, remove the :key="device.id" from my fiddle and see what happens in the console
Selecting a key
As the key must uniquely identify your device, you should not use index as a key as device index in the array changes whenever you copy a device
Additionally, an ID field is preferred because there is no guarantee that your devices names are unique. What if you initialise the list with
[
{ name: "a"},
{ name: "b"},
{ name: "a"}
]
From a functional point of view, this is correct.
When working with Vue and lists you should add a key prop to the element with v-for. Using the key like this, let's Vue know that you mean a specific element.
<div class="device" v-for="device in devices" :key="device.name">
I believe the reason for this is that due to performance reasons Vue by default adds a new element as the last element and then updates the data in the other nodes. Thus, the new element that you add is actually the last one in the list which has open set as true.
A little addition to your code:
Instead of providing the "device" object and searching for its index you could just pass the index directly.
This is what i mean: jsFiddle
Vue.component("Accordion", {
template: "#accordion-template",
data: function() {
return {
open: true
}
},
methods: {
toggle: function() {
this.open = !this.open;
}
}
});
new Vue({
el: '#vue-root',
data: {
devices: [{
name: "a",
description: "first"
},
{
name: "b",
description: "second"
},
{
name: "c",
description: "third"
},
]
},
methods: {
copy: function(index) {
var device = this.devices[index];
var copy = {
name: device.name + "_copy",
description: device.description + "_copy"
};
this.devices.splice(index + 1, 0, copy);
},
remove: function(index) {
this.devices.splice(index, 1);
}
}
});
.device {
margin: 10px 0;
}
.active {
display: block;
}
.hidden {
display: none;
}
div.device {
border: 1px solid #000000;
box-shadow: 1px 1px 2px 1px #a3a3a3;
width: 300px;
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="vue-root">
<div class="device" v-for="(device, index) in devices" :key="device.name">
<accordion>
<div slot="acc-head">
<span>{{ device.name }}</span><br/>
<button #click="copy(index)">copy</button>
<button #click="remove(index)">remove</button>
</div>
<div slot="acc-body">
{{ device.description }}
</div>
</accordion>
</div>
</div>
<script type="text/x-template" id="accordion-template">
<div>
<slot name="acc-head"></slot>
<button #click="toggle">Open: {{ open }}</button>
<div :class="open ? 'active' : 'hidden'">
<slot name="acc-body"></slot>
</div>
</div>
</script>
This may have been answered before, but I have been unable to find an answer that works in this specific situation.
I'm new to Vue and trying to build a Todo list in which I can click on a list item when it is complete, changing or adding a class that will change the style of the item.
I guess I don't fully understand how the scopes work together between the main Vue and a component. The code I have right now does absolutely nothing. I have tried moving methods between the main and component, but it always gives me some error.
I guess I'm just looking for some guidance as to how this should be done.
Vue.component('todo-item', {
props: ['todo'],
template: '<li>{{ todo.id + 1 }}. {{ todo.text }}</li>'
})
var app = new Vue({
el: '#app',
data: {
isDone: false,
todos: [
{ id: 0, text: 'This is an item to do today' },
{ id: 1, text: 'This is an item to do today' },
{ id: 2, text: 'This is an item to do today' },
{ id: 3, text: 'This is an item to do today' },
{ id: 4, text: 'This is an item to do today' },
{ id: 5, text: 'This is an item to do today' }
]
},
methods: {
markDone: function(todo) {
console.log(todo)
this.isDone = true
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<div class="content">
<ul class="flex">
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
#click="markDone"
:class="{'done': isDone}"
></todo-item>
</ul>
</div>
</div>
Thanks for the help, guys.
You were getting so close! You simply had your :class="{'done': isDone}" #click="markDone" in the wrong place!
The important thing to remember with components is that each one has to have their own data. In your case, you were binding all todo's to your root Vue instance's done variable. You want to instead bind each one to their own done variable in their own data.
The way you do this is by creating a function version of data that returns individual data for each component. It would look like this:
data () {
return {
isDone: false,
}
},
And then you move the :class="{'done': isDone} from the todo to the li internal to it:
<li :class="{'done': isDone}">{{ todo.id + 1 }}. {{ todo.text }}</li>
Now we have the 'done' class depending on an individual data piece for each individual todo element. All we need to do now is be able to mark it as complete. So we also want each todo component to have it's own method to do so. Add a methods: object to your todo component and move your markDone method there:
methods: {
markDone() {
this.isDone = true;
},
}
Now move the #click="markDone" to the li as well:
<li :class="{'done': isDone}" #click="markDone">{{ todo.id + 1 }}. {{ todo.text }}</li>
And there you go! Now you should be able to create as many todo's as you want, and mark them all complete!
Bonus:
Consider changing your function to toggleDone() { this.isDone = !this.isDone; }, that way you can toggle them back and forth between done and not done!
Full code below :)
Vue.component('todo-item', {
props: ['todo'],
template: `<li :class="{'done': isDone}" #click="toggleDone">{{ todo.id + 1 }}. {{ todo.text }}</li>`,
data () {
return {
isDone: false,
}
},
methods: {
toggleDone() {
this.isDone = !this.isDone;
},
}
})
var app = new Vue({
el: '#app',
data: {
isDone: false,
todos: [
{ id: 0, text: 'This is an item to do today' },
{ id: 1, text: 'This is an item to do today' },
{ id: 2, text: 'This is an item to do today' },
{ id: 3, text: 'This is an item to do today' },
{ id: 4, text: 'This is an item to do today' },
{ id: 5, text: 'This is an item to do today' }
]
},
methods: {
markDone: function(todo) {
console.log(todo)
this.isDone = true
}
}
})
.done {
text-decoration: line-through;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<div class="content">
<ul class="flex">
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
></todo-item>
</ul>
</div>
</div>
First of all, your current implementation will affect all items in the list when you mark one item as done because you are associating a single isDone property to all the items and when that property becomes true, it will be applied to all the items in your list.
So to fix that, you need to find a way to associate done to each item. And because your item is an object, you just assign a new property done dynamically and set the value to true which means it is marked as done. It will be very confusing to just explain it, so I included a full example using your existing code.
See this JS Fiddle: https://jsfiddle.net/eywraw8t/205021/
In your code, which are handling with the click event is the <li> element, but your are trying to handle it in the root of your component, there're a few ways to solve this
Use native modifier
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
#click.native="markDone"
:class="{'done': isDone}"
>
</todo-item>
You can find more info here
https://v2.vuejs.org/v2/guide/migration.html#Listening-for-Native-Events-on-Components-with-v-on-changed
Emit from component the click event
Vue.component('todo-item', {
props: ['todo'],
template: '<li #click="click()">{{ todo.id + 1 }}. {{ todo.text }}</li>',
methods: {
click () {
this.$emit('click')
}
}
})
By the way, in your current code once you click in one todo all the todos will be "marked as done" as you are using just one variable for all of them.
In the following example neither of the v-if related divs seem to get rendered before or after clicking the Add button. It seems like Vue.js isn't running any updates when the pizzas JSON object is updated.
Is there a solution to this problem without resorting to changing the pizzas variable into being an array?
<div id="app">
<div v-for="pizza in pizzas">
{{ pizza }}
</div>
<div v-if="totalPizzas === 0">
No pizza. :(
</div>
<div v-if="totalPizzas > 0">
Finally, some pizza! :D
</div>
<button #click="add">Add</button>
</div>
var app = new Vue({
el: '#app',
data: {
pizzas: {}
},
methods: {
add: function() {
this.pizzas['pepperoni'] = { size: 16, toppings: [ 'pepperoni', 'cheese' ] };
this.pizzas['meaty madness'] = { size: 14, toppings: [ 'meatballs', 'sausage', 'cajun chicken', 'pepperoni' ] };
},
totalPizzas: function() {
return Object.keys(this.pizzas).length;
}
}
});
There are several things to be improved in your code. Most of them are about syntax. For example, methods should be called, but computed properties can be queried directly: that's why it's #click="add()", but totalPizzas === 0 makes sense only if it's a computed property.
The crucial thing to understand, however, is how reactivity works in VueJS. See, while you change your object innards, adding new properties to it, this change is not detected by VueJS. Quoting the docs:
Vue does not allow dynamically adding new root-level reactive
properties to an already created instance. However, it’s possible to
add reactive properties to a nested object using the Vue.set(object, key, value) method:
Vue.set(vm.someObject, 'b', 2)
You can also use the vm.$set instance method, which is an alias to the
global Vue.set:
this.$set(this.someObject, 'b', 2)
Sometimes you may want to assign a number of properties to an existing
object, for example using Object.assign() or _.extend(). However, new
properties added to the object will not trigger changes. In such
cases, create a fresh object with properties from both the original
object and the mixin object:
// instead of `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
And this is how it might work:
var app = new Vue({
el: '#app',
data: {
pizzas: {}
},
computed: {
totalPizzas: function() {
return Object.keys(this.pizzas).length;
}
},
methods: {
add: function() {
this.pizzas = Object.assign({}, this.pizzas, {
pepperoni: { size: 16, toppings: [ 'pepperoni', 'cheese' ] },
['meaty madness']: { size: 14, toppings: [ 'meatballs', 'sausage', 'cajun chicken', 'pepperoni' ] }
});
},
}
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.js"></script>
<div id="app">
<div v-for="pizza in pizzas">
Size: {{ pizza.size }} inches
Toppings: {{ pizza.toppings.join(' and ') }}
</div>
<div v-if="totalPizzas === 0">
No pizza. :(
</div>
<div v-if="totalPizzas > 0">
Finally, some pizza! :D
</div>
<button #click="add()">Add</button>
</div>