VueJS v-for with method always returns false


#1

I am using a v-for to loop through my messages. Now what I want is to check for each message date and if the day varies, display the day, add it to my data, and continue. This means every day should only be displayed once.

<div class="spacer">
    <span class="grouped-date" v-if="displayPostDate(message.created_at)">
        {{ message.created_at }}
    </span>
</div>

The weird part is that in my method displayPostDate() I can check for:

if(!this.datesDone.includes(d)){}

and this works successfully when I console.log it. But no matter what I return, VueJS never parses the right data.

For example:

let d = date.substring(0, 10);

if(!this.datesDone.includes(d)) {

	console.log('not present');
	this.datesDone.push(date.substring(0, 10));
	
	return 'display';

} else {

	console.log('present');
	return 'dont';

}

No matter what I return, my v-if=“displayPostDate(message.created_at)”> will not respond to it since.

Can anyone please tell me how I can fix this?

My full method:

displayPostDate(date) {
	let d = date.substring(0, 10);

	if(!this.datesDone.includes(d)) {
		this.datesDone.push(date.substring(0, 10));
		return true;

	} else {
		return false;
	}
}

My template:

<template v-for="(message, index) in messages">
    {{ displayPostDate( message.created_at) }}
</template>

#2

What’s your sample data look like?

Btw, it’s easier to read truthy statements. Maybe just personal preference, but something to consider.

displayPostDate(date) {
  let d = date.substring(0, 10);

  if(this.datesDone.includes(d)) return false;

  this.datesDone.push(date.substring(0, 10));
  return true;
}

#3

I’m wondering if this could be due to this, from the docs:

v-if is also lazy : if the condition is false on initial render, it will not do anything - the conditional block won’t be rendered until the condition becomes true for the first time.

But here the condition is a function call, and this function is only called at the initial render.

If for example you’re getting your messages from mapGetters in vuex (or from an async API call), they may not be there at the time the component is rendered, so your function will return false.

This also explains why it works in your manual tests: by the time you’ve opened the console, the data should already be there.

I’m not an expert myself so might be way off, but it makes sense to me.


#4

This is true if the data isn’t reactive, however if it is it will cause the component to re-render which will call the method again to evaluate.

So if messages is an empty array on initial render, but then populated later the component will re-render and re-evaluate the method.


#5

Thanks for the clarification @JamesThomson, but I’ve read that methods are not reactive by nature and that’s what computed is for?

In any case, I have another theory: if this refers to the individual component that is message, then of course the array that is datesDone will always NOT contain said date, because this is constructed individually for each message object, and each of them is unaware of other data that might exist in the sibling-components of type message.

In any case it would really help if @GroundZero includes his component hierarchy and their data definitions.


#6

You are right, methods aren’t reactive, but they will be re-evaluated if something on the component that is reactive causes the component to re-render. e.g. if another message is added to the messages array.

computed are reactive based on their deps, but also cache their values. So, while the component may re-render which would cause the method to re-evaluated, the computed property would not need to re-evaluate (assuming none of its deps have changed) and would instead return the cached value, thus making them more efficient.


#7

could you please give me the data(messages) value.


#8

Here’s my solution, test and working with my own Vue app that uses todos instead of messages. I’ve omitted the irrelevant parts. These changes should be made on the parent component that renders the message array.

    export default {
        name: 'dashboard',
        data: () => ({
            uniqueDates: [], // create a unique dates array
        }),
        components: {
            ToDoItem,
            Stats,
            toDoForm
        },
        computed: mapGetters(['todos']), // our todos are coming from the vuex
        // it's better for watch for changes in todos because
       // todos may not be available at the time of mounting
        watch: {
            todos: {
                handler() {
                    this.todos.forEach(todo => { // use arrow function so `this` isn't undefined
                            let d = todo.created_at.substring(0, 10);
                            if (this.uniqueDates.includes(d)) {
                                todo.showDate = false
                            }
                            else {
                                todo.showDate = true
                                this.uniqueDates.push(d)
                            }

                        }
                    )}, deep: true

            }
        }
    }

All you have to do in the child component is:

<p v-if="todo.showDate">
    created at: {{todo.created_at.substring(0,10)}}
</p>

Note that this property isn’t reactive due to this caveat.

Edit: The reactive version of this looks like this:

watch:{
            todos: {
                handler() {
                    this.uniqueDates = []
                    this.todos.forEach(todo => {
                            let d = todo.created_at.substring(0, 10);
                            if (this.uniqueDates.includes(d)) {
                                this.$set(todo, 'showDate', false) // overcome reactivity caveats when creating new object properties
                            }
                            else {
                                this.$set(todo, 'showDate', true) // overcome reactivity caveats when creating new object properties
                                this.uniqueDates.push(d)
                            }
                        }
                    )
                }
            }, deep: true, immediate:true
        }

@JamesThomson, any insights?


#9

Looks pretty good. I generally avoid watchers (at least use them sparingly) so my initial reaction is that a watcher like that could become a rather expensive operation if its run every time a new todo is added/edited/removed. It would be fine for a list of a dozen or so, but imagine a list of 1000 or more (unlikely, perhaps, but still something to consider).

One thing, this.uniqueDates = [] looks like it could just be local to the handler. Does it need to be part of the instances reactive data?


#10

You’re absolutely right about that. In the new implementation which is reactive uniqueDates is re-initialized on every run so it doesn’t need to be held in data. Thanks for the feedback!

I generally avoid watchers (at least use them sparingly) so my initial reaction is that a watcher like that could become a rather expensive operation if its run every time a new todo is added/edited/removed. It would be fine for a list of a dozen or so, but imagine a list of 1000 or more (unlikely, perhaps, but still something to consider).

Because the data may not be available on initial load, I thought watchers could be of good use. I’m a total Vue noob though, so I’ll note what you wrote.


#11

They can be, and depending on the use case you can use them for various things. One such case is if you need to run something, but require a certain state to be set first.

e.g.

if (this.user.isAuthenticated) {
  this.getData();
} else {
  let unwatch = this.$watch('user.isAuthenticated', () => {
    this.getData();
    unwatch(); // Remember to always clean up ;)
  }); 
}

#12

You’re fast @JamesThomson! Thanks for being so helpful, I’m learning a ton from you.


#13

Thank you so much for your replies everyone, I will try to get my code to work with your answers and I will post the outcome. Lovely to see so many replies :slight_smile: