ofey
ofey

Reputation: 3347

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

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

Upvotes: 58

Views: 151901

Answers (10)

Elya Livshitz
Elya Livshitz

Reputation: 1

You can work around layout issues caused by the extra wrapper element by using CSS's display: contents. This makes the container “invisible” to layout and flexbox, allowing you to nest the v-if without altering your layout.

For example:

<div style="display: contents;" v-for="item in items">
  <div v-if="item.type === 'aaa'">AAA</div>
  <div v-else>BBB</div>
</div>

This approach maintains the desired structure without adding extra DOM elements that could disrupt your styling.

Upvotes: 0

pxeba
pxeba

Reputation: 1806

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.

Upvotes: 54

Snakeoi
Snakeoi

Reputation: 1

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>

Upvotes: 0

inTheFlow
inTheFlow

Reputation: 357

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>

Upvotes: 14

Agus Mathew
Agus Mathew

Reputation: 941

<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>

Upvotes: 7

Ludo - Off the record
Ludo - Off the record

Reputation: 5543

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> ...

Upvotes: 3

DobleL
DobleL

Reputation: 3928

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

Upvotes: 93

Rory Callaghan Cullen
Rory Callaghan Cullen

Reputation: 59

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>'
})

Upvotes: 5

vpalade
vpalade

Reputation: 1437

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>

Upvotes: 5

Raith
Raith

Reputation: 528

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'.

Upvotes: 2

Related Questions