v-for and v-if not working together in vue.js
Asked Answered
S

9

53

A form is used to submit text and two options which tell vue which column to display the text in. When the col2 radio button is checked the submitted text should display in column 2. This is not happening, on column 1 text is displaying.

I have two radio buttons which should pass the value 'one' or 'two' to a newInfo.option On submnit a method pushed the form data to the array 'info'.

<input type="radio" id="col1" value="one" v-model="newInfo.col">
<input type="radio" id="col2" value="two" v-model="newInfo.col">

This data is being pushed to the array 'info' correctly and I can iterate through it. I know this is working because I can iterate through the array, an console.log all the data in it. All the submitted form data is there.

Next I iterate through this array twice in the template. Once for info.col==="one" and the other iteration should only display when info.col==="two". I am using a v-for and v-if together, which the vue.js documentation says is ok to do,

https://v2.vuejs.org/v2/guide/conditional.html#v-if-with-v-for

<div class="row">
            <div class="col-md-6">
                <ol>
                    <li v-for="item in info" v-if="item.col==='one'">
                        text: {{ item.text }}, col: {{ item.col }}
                    </li>
                </ol>
            </div>
            <div class="col-md-6">
                <ol>
                    <li v-for="item in info" v-if="!item.col==='two'">
                        text: {{ item.text }}, col: {{ item.col }}
                    </li>
                </ol>
            </div>
        </div>

The full vue.js code is on github here

And it is running on gh-pages here

Sextodecimo answered 22/2, 2018 at 17:21 Comment(4)
I had the same problem. This link explains why it isn't working an what you should do for an array as well as an objectCreamy
How about using v-show instead of v-if ? I know the element will be still there but it will be hidden.Infinitude
You can use Javascript within the v-for directive. For example, item in myFilter(info).Driving
That link to the Guid you provide also states that v-if has higher precendence than v-for. That would mean it'd try to evaluate item.col before item even has a value.Leptospirosis
A
12

You could also use JavaScript in your template to filter the array elements of the v-for. Instead of v-for="item in infos" you could narrow down the info-array to v-for="item in infos.filter(info => info.col === 'one')".

I renamed your info-array to infos to improve readability of my suggestion because of the usage of info in the callbacks.

<div class="row">
    <div class="col-md-6">
        <ol>
            <li v-for="item in infos.filter(info => info.col === 'one')">
                text: {{ item.text }}, col: {{ item.col }}
            </li>
        </ol>
    </div>
    <div class="col-md-6">
        <ol>
            <li v-for="item in infos.filter(({ col }) => col === 'two')">
                text: {{ item.text }}, col: {{ item.col }}
            </li>
        </ol>
    </div>
</div>

Aldaaldan answered 29/5, 2022 at 12:42 Comment(2)
True you can put the filter logic in the template, but this is less elegant (and less Vue-like) than putting the filter logic in a computed property as in DobleL's answer.Piscator
I agree. Just wanted to add something not mentioned, yet. I had a similar problem, why I ended up here. In my case, this was the more readable approach instead of introducing new variable names aka computed properties because it was more useful to see where the actual array is coming from. A descriptive variable name for the computed property would have been just as long as the written out filter method. Maybe this could be true to some others, too.Aldaaldan
P
91

Why don't use the power of Computed Properties ?

computed: {
  infoOne: function () {
    return this.info.filter(i => i.col === 'one')
  },
  infoTwo: function () {
    return this.info.filter(i => i.col === 'two')
  }
}

Then on each list just iterate over its respective property without the need to check. Example

<ol>
   <li v-for="item in infoOne">{{item}}</li>
</ol>

Here the working fiddle

Pegasus answered 22/2, 2018 at 18:10 Comment(5)
Nice answer, and answer to your question cause I am very new to vue and still have loads to pick up on. But thanks, your answer is very interestingSextodecimo
glad i've helpedPegasus
This is the advice of the official Vue style guide, and is categorized as "essential" (as opposed to "strongly recommended", "recommended" or "use with caution").Towel
@Pegasus Hey, I definitelly like your answer and I have modified it. I added "delete" button next to each list item, so on button click I delete that index from the list. The problem is that we iterate over two lists from computed properties and basically we have repeating indexes (check out print of list items and you will understand) so it deletes items from wrong category. I was thinking of way to solve this, but I would have to use v-for and v-if together which is not recommended. Do you have any advice or solutions? Thanks in advance! Here is my fiddlePeachey
That helped thank you , but I'm confused why this wont work with Boolean ? I mean the filtering !Underbid
E
47

From Vue docs:

When they exist on the same node, v-if has a higher priority than v-for. That means the v-if condition will not have access to variables from the scope of the v-for:


<!--
This will throw an error because property "todo"
is not defined on instance.
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

This can be fixed by moving v-for to a wrapping tag (which is also more explicit):

<template v-for="todo in todos">   
  <li v-if="!todo.isComplete">
     {{ todo.name }}   
  </li> 
</template> 

If you don't mind your element remaining present in the html as "display:none" you can combine v-show with v-for.

Ezarras answered 22/3, 2022 at 21:4 Comment(3)
the <template v-for="todo in todos"> helped me! Wish you have a great day!Sparhawk
but your link is wrong thoughSparhawk
The option with <template v-for... is the best!Darrel
A
12

You could also use JavaScript in your template to filter the array elements of the v-for. Instead of v-for="item in infos" you could narrow down the info-array to v-for="item in infos.filter(info => info.col === 'one')".

I renamed your info-array to infos to improve readability of my suggestion because of the usage of info in the callbacks.

<div class="row">
    <div class="col-md-6">
        <ol>
            <li v-for="item in infos.filter(info => info.col === 'one')">
                text: {{ item.text }}, col: {{ item.col }}
            </li>
        </ol>
    </div>
    <div class="col-md-6">
        <ol>
            <li v-for="item in infos.filter(({ col }) => col === 'two')">
                text: {{ item.text }}, col: {{ item.col }}
            </li>
        </ol>
    </div>
</div>

Aldaaldan answered 29/5, 2022 at 12:42 Comment(2)
True you can put the filter logic in the template, but this is less elegant (and less Vue-like) than putting the filter logic in a computed property as in DobleL's answer.Piscator
I agree. Just wanted to add something not mentioned, yet. I had a similar problem, why I ended up here. In my case, this was the more readable approach instead of introducing new variable names aka computed properties because it was more useful to see where the actual array is coming from. A descriptive variable name for the computed property would have been just as long as the written out filter method. Maybe this could be true to some others, too.Aldaaldan
O
7
<div class="row">
    <div class="col-md-6">
        <ol>
            <li v-for="item in info">
                <template v-if="item.col==='one'">
                    text: {{ item.text }}, col: {{ item.col }}
                <template>
            </li>
        </ol>
    </div>
    <div class="col-md-6">
        <ol>
            <li v-for="item in info">
                <template v-if="!item.col==='two'">
                    text: {{ item.text }}, col: {{ item.col }}
                <template>
            </li>
        </ol>
    </div>
</div>
Operon answered 10/8, 2020 at 12:17 Comment(2)
You should comment on your solution to provide more insights mate :) "Code only" answers are not very fun :)Ilene
Better solution would be to swap template with li. This way you avoid empty li elements.Castilian
F
5

Remove ! from second if v-if="item.col==='two'"

better you can do this way (to iterate only once):

<div class="row" v-for="item in info">
            <div class="col-md-6">
                <ol>
                    <li v-if="item.col==='one'">
                        text: {{ item.text }}, col: {{ item.col }}
                    </li>
                </ol>
            </div>
            <div class="col-md-6">
                <ol>
                    <li v-if="item.col==='two'">
                        text: {{ item.text }}, col: {{ item.col }}
                    </li>
                </ol>
            </div>
        </div>
Fiddlehead answered 22/2, 2018 at 17:29 Comment(2)
Ah! I see it now thanks and yes your way means only iterating through once. Thanks a lotSextodecimo
Actually your code will stagger the rows. No quite what i want. But thanksSextodecimo
I
5

If for some reason, filtering the list is not an option, you can convert the element with both v-for and v-if in to a component and move the v-if in to the component.

Original Example

Original Loop

<li v-for="item in info" v-if="item.col==='one'">
  text: {{ item.text }}, col: {{ item.col }}
</li>

Suggested Refactor

Refactored Loop

<custom-li v-for="item in info" :visible="item.col==='one'">
  text: {{ item.text }}, col: {{ item.col }}
</custom-li>

New Component

Vue.component('custom-li', {
  props: ['visible'],
  template: '<li v-if="visible"><slot/></li>'
})
Immunology answered 3/12, 2020 at 19:53 Comment(1)
I liked the refactored suggestion. I used it for adding a CSS class to make it hidden. Thanks!Filtrate
H
3

Computed

In most cases a computed property is indeed the best way to do it like DobleL said, but in my own case I'm having a v-for within another v-for so then the computed doesn't make sense for the second v-for.

V-show

So instead of using v-if which has a higher priority than v-for (as mentioned by Mithsew), an alternative would be to use v-show which doesn't have that higher priority. It basically does the same, and works in your case.

No wrapper

Using v-show avoids having to add a useless wrapper element as I've seen in some answers, which in my case was a requirement to avoid messing up CSS selectors and html structure.

Downside of v-show

The only downside of using v-show is that the element will still be added in your HTML, which in my own case still messes up CSS :first selectors for example. So I personally actually went for the .filter() solution mentioned by inTheFlow. But in most basic cases, you can definitely use v-show to solve this problem.

<div class="row">
  <div class="col-md-6">
      <ol>
          <li v-for="item in info" v-show="item.col==='one'">
              text: {{ item.text }}, col: {{ item.col }}
          </li>
      </ol>
  </div>
  <div class="col-md-6">
      <ol>
          <li v-for="item in info" v-show="item.col!=='two'">
              text: {{ item.text }}, col: {{ item.col }}
          </li>
      </ol>
  </div>
</div>

If you are curious why the hell I'm using a v-for within another v-for, here's a stripped-down version of my use case:

It's a list of conversations (which is a computed property), then displaying the avatars of all the participants within each conversation, except for the avatar of the user who is currently viewing it.

<a v-for="convo in filteredConversations" class="card">
  <div class="card-body">
      <div class="row">
          <div class="col-auto">
            <div class="avatar-group">
                <div class="avatar" v-for="participant in convo.participants.filter(info => !thatsMe(info))">
                    <img :src="userAvatarUrl(participant)" :alt="participant.name" class="avatar-img">
                </div>
            </div>
          </div> ...
Havildar answered 11/8, 2022 at 8:25 Comment(0)
M
2

Your second check is !item.col==='two' and would only display if it does not equal 'two'.

EDIT: The ! not operator is likely binding more closely than === so that will always return false. Add brackets to control the order of application. I say likely because it may be a bit of Vue magic that I'm not familar with, rather than a pure JavaScript expression.

I think you want to remove that exclamation mark. Or to make it !(item.col==='one') to display for any value other than 'one'.

Marlie answered 22/2, 2018 at 17:27 Comment(0)
A
0

For me, the best option whas to use filter.

<div v-for="target in targets.filter((target) => target.zone_id == zone.id)">
  {{ target.id}}
</div>
Adduct answered 4/12, 2023 at 12:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.