Template renders twice in vue.js - vue.js

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>

Related

Vue 2 call method in v-for performance issue

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>

Getting data from component VueJS

Very simple question. I'm learning VueJS and have created a simple component:
Vue.component('blog-post', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})
I then have parsed some data to it like this:
new Vue({
el: '#blog-post-demo',
data: {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
})
My question is how can get the title of a specefic element based on the id in my HTML? For now I can only render through the items and get them all, but I want to be able to specify which title I want to display based on the Id. Here is my HTML which gives me all the data:
<div id="blog-post-demo">
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:title="post.title"
></blog-post>
</div>
You can achieve with COMPUTED property, like that
<template>
<div id="blog-post-demo">
<p v-for="post in thisOne" :key="post.id" >
{{post.title}}
</p>
</div>
</template>
<script>
export default {
el: '#blog-post-demo',
data() {
return {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
},computed: {
thisOne(){
return this.posts.filter(x => x.id === 3); /*choose your id*/
}
}
};
</script>
Or you can use event too to select the id of the posts to display (more dynamically)
Tip: If you start with VueJS, learn about the properties of VueJs (DATA, COMPUTED, CREATED, METHOD) and look at the uses and strengths of each one. For my part, the VueJS site is very very well done for beginners: https://v2.vuejs.org/v2/guide/
I'm not sure if I understand correctly what you want to do. But if you want to go through all posts and display title of particular post then you can try this way:
<blog-post
v-for="post in posts"
:key="post.id"
:title="setTitle(post)"
/>
(: instead of :v-bind it's a short form, also if you don't pass slots in your component you can go with self-closing tag)
Then in your methods section you can create a method:
setTitle(post) {
if(post.id === 2) return post.title
}

How is the method Total being called in this example

I see in the code below how the list item's class and state is being modified but I don't understand where or how the total() method is being triggered. The total is added to the markup in the <span>{{total() | currency}}</span> but there is no click event or anything reactive that I see in the code that is bound to it.
<template>
<!-- v-cloak hides any un-compiled data bindings until the Vue instance is ready. -->
<form id="main" v-cloak>
<h1>Services</h1>
<ul>
<!-- Loop through the services array, assign a click handler, and set or
remove the "active" css class if needed -->
<li
v-for="service in services"
v-bind:key="service.id"
v-on:click="toggleActive(service)"
v-bind:class="{ 'active': service.active}">
<!-- Display the name and price for every entry in the array .
Vue.js has a built in currency filter for formatting the price -->
{{service.name}} <span>{{service.price | currency}}</span>
</li>
</ul>
<div class="total">
<!-- Calculate the total price of all chosen services. Format it as currency. -->
Total: <span>{{total() | currency}}</span>
</div>
</form>
</template>
<script>
export default {
name: 'OrderForm',
data(){
return{
// Define the model properties. The view will loop
// through the services array and genreate a li
// element for every one of its items.
services: [
{
name: 'Web Development',
price: 300,
active:true
},{
name: 'Design',
price: 400,
active:false
},{
name: 'Integration',
price: 250,
active:false
},{
name: 'Training',
price: 220,
active:false
}
]
}
},
// Functions we will be using.
methods: {
toggleActive: function(s){
s.active = !s.active;
},
total: function(){
var total = 0;
this.services.forEach(function(s){
if (s.active){
total+= s.price;
}
});
return total;
}
},
filters: {
currency: function(value) {
return '$' + value.toFixed(2);
}
}
}
</script>
EDIT:
Working example https://tutorialzine.com/2016/03/5-practical-examples-for-learning-vue-js
So I believe the explanation for what is happening is that data's services object is reactive. Since the total method is being bound to it, when the toggleActive method is being called, it is updating services which causes the total method to also be called.
From the docs here 'How Changes Are Tracked' https://v2.vuejs.org/v2/guide/reactivity.html
Every component instance has a corresponding watcher instance, which records any properties “touched” during the component’s render as dependencies. Later on when a dependency’s setter is triggered, it notifies the watcher, which in turn causes the component to re-render.
Often I find simplifying what is going on helps me understand it. If you did a very simplified version of above it might look like this.
<div id="app">
<button #click="increment">Increment by 1</button>
<p>{{total()}}</p>
</div>
new Vue({
el: "#app",
data: {
counter: 0,
},
methods: {
increment: function(){
this.counter += 1;
},
total: function(){
return this.counter;
}
}
})
working example: https://jsfiddle.net/skribe/yq4moz2e/10/
If you simplify it even further by putting the data property counter in the template, when its value changes, you would naturally expect the value in the template to also be updated. So this should help you understand why the total method gets called.
<div id="app">
<button #click="increment">Increment by 1</button>
<p>{{counter}}</p>
</div>
new Vue({
el: "#app",
data: {
counter: 0,
},
methods: {
increment: function(){
this.counter += 1;
},
}
})
working example: https://jsfiddle.net/skribe/yq4moz2e/6/
When you update the data, the template in the components rerendered. That means that the template will trigger all methods bind to the templates. You can see it by adding dynamic date for example.
<div id="app">
<button #click="increment">Increment by 1</button>
<p>{{total()}}</p>
<p>
// Date will be updated after clicking on increment:
{{date()}}
</p>
</div>
new Vue({
el: "#app",
data: {
counter: 0,
},
methods: {
increment: function(){
this.counter += 1;
},
total: function(){
return this.counter;
},
date: function() {
return new Date();
}
}
})

Marking a Vue Todo List Item as done

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.

Vue slot-scope with v-for, data changed but not re-rendered

I have a question while I'm studying vuejs2
I made an example with slot-scope && v-for, but it has an error which I can't understand.
Here is example code
https://jsfiddle.net/eywraw8t/6839/
app.vue
<template id="list-template">
<div>
<ul>
<slot name="row" v-for="item in list" v-bind=item />
</ul>
</div>
</template>
<div id="app">
<list-component :list="list">
<li slot="row" slot-scope="item">
{{item.name}}
</li>
</list-component>
</div>
Vue.component('list-component', {
template: '#list-template',
props: {
list: {
type: Array
}
}
});
new Vue({
el: '#app',
data () {
return {
list: [
{id: 1, name: 'Apple'},
{id: 2, name: 'Banana'},
{id: 3, name: 'Cherry'},
{id: 4, name: 'Durian'},
{id: 5, name: 'Eggplant'}
]
}
},
methods: {
h (item) {
item.name = item.name.toUpperCase()
console.log('Changed!')
console.log(item)
}
}
});
Strange thing is, the method 'h' is triggered and then, the console said 'Changed!' and data also changed but, the view is not re-rendered.
What am I missing? I think slot-scoped object data is not referencing the original object data.
What should I do to modify the original data?
Thanks for reading this question.
You are directly trying to modify the value of an item in an array. Due to some limitations vue cannot detect such array modifications.
So update your code to use vm.$set() to make chamges to the array item.
methods: {
h (item) {
let i = this.items.findIndex((it) => it.id === item.id);
this.$set(this.list, i, {...item, name: item.name.toUpperCase()});
console.log(item.id)
}
Here is the updated fiddle