V-if inside v-for - display list of items in two columns

Viewed 13799

I'm sorry for posting this, - because I can see that many questions similar to this one has been asked several time. Here's the ones that came closes to helping me - and why they didn't:

  • This one is because the calculation shouldn't be done in the rendering, but rather in the methods/computed section. That doesn't help me.
  • This one is using two different templates, writing the v-if on the template-tag. This would seem foolish in my case, since the two templates would be 98% identical.
  • This Medium-article addresses a problem very very close to mine. However, - it's a filtering of users in his case (which is solved by computed properties), and not an if-clause that inserts a snippet of code at a certain iteration (which is what I think I'm looking for).

The problem

I have a list of items, being pulled from an API, - so the amount will change. I want them displayed in two columns as such:

-----------------
| Item1   Item5 |
| Item2   Item6 |
| Item3   Item7 |
| Item4         |
-----------------

I'm looping through them using a v-for loop.

My attempts

  1. Using pure CSS with display: flex

But that can only do this:

-----------------
| Item1   Item2 |
| Item3   Item4 |
| Item5   Item6 |
| Item7         |
-----------------
  1. Using CSS with column-count: 2;

But that breaks of the column mid-element, regards of display: block; overflow: hidden; and many other attemps. It should be said, that the height of these elements can vary.

  1. So I gave up on fixing it using CSS.

If it had been php, then I'd simply do something like this:

<?php
if( $index == count( $items)/2 ):
  echo '</div>';
  echo '</div>';
  echo '<div class="col-md-6">';
  echo '<div class="item-container">';
endif;
?>

... But it's not. And I'm looking for the vue-alternative. I tried this:

{{#if key === Number( items.length / 2 ) }}
  </div>
  </div>
  <div class="col-md-6">
  <div class="item-container">
{{/if}

But it doesn't work. And as far as I can tell, then it's not 'the vue way' of doing it. But I can't figure out what is. :-/

Does any such thing exist?

A simplification of my current code

<div class="col-md-12">
    <div class="items-container">
        <div class="item-container" v-for="item, key in items['data']">
            <!-- A BUNCH OF ITEM-INFO -->
        </div><!-- /.item-container -->
    </div><!-- /.items-container -->
</div><!-- /.col-md-12 -->
3 Answers

What I would do is create a computed property dividing (or chunking) the items array into the appropriate number of columns.

Here's an example that uses a flexbox layout and one extra column element.

new Vue({
  el: 'main',
  data: {
    items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6', 'Item 7'],
    cols: 2
  },
  computed: {
    columns () {
      let columns = []
      let mid = Math.ceil(this.items.length / this.cols)
      for (let col = 0; col < this.cols; col++) {
        columns.push(this.items.slice(col * mid, col * mid + mid))
      }
      return columns
    }
  }
})
.container {
  display: flex;
  border: 1px solid;
}
.col {
  margin: 10px;
  border: 1px solid;
  flex-grow: 1;
  display: flex;
  flex-direction: column;
}
.item-container {
  border: 1px solid;
  padding: 5px;
  margin: 5px;
}
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
<main>
<p><label>Columns:<label> <input type="number" v-model="cols"></p>
<div class="container">
  <div class="col" v-for="column in columns">
    <div class="item-container" v-for="item in column">{{item}}</div>
  </div>
</div>
</main>

If you'd like a less verbose way of chunking the items array, see Split array into chunks

Its good to see someone who stumbled across the same problem as me. I had to position 6 items in each column. I sliced the API-response into the columns and printed them afterwards.

let allCategory = response.body.Categories.slice(); //clone
while (allCategory.length > 0) {
  let chunk = allCategory.splice(0,6);
  this.ColArray.push(chunk);
}  

ColArray is an array that then will contain arrays of the columns. It would look like this:

{
  ColArray: [
    Column1: [
      Item1,
      Item2,
      Item3,
    ],
    Column2: [
      ...
    ]
  ]
}

In Vue it will just be looped through, like this:

<div v-for="(col,colIndex) in ColArray" :key="'cate_col'+colIndex" class="col-md-2">
  <div v-for="(row,rowIndex ) in col"   :key="'cate_row'+colIndex + rowIndex" class="row">
    {{row}}
  </div>
</div>

Here is a sample fiddle :

https://jsfiddle.net/keysl183/50wL7mdz/775484/

For anyone who's attempting to achieve the same result (divide items into columns), but splitting an object instead of an array.

This solution is based in Phil's selected answer.

It uses Object.keys to loop through the object and Object.entries to return an array out of the object. Instead of accessing through index in your html template, you would do it by key.

new Vue({
    el: 'main',
    data: {
        items: { 
          item1: { name: 'Item 1' }, 
          item2: { name: 'Item 2' }, 
          item3: { name: 'Item 3' },
          item4: { name: 'Item 4' },
          item5: { name: 'Item 5' },
          item6: { name: 'Item 6' },
          item7: { name: 'Item 7' },
          item8: { name: 'Item 8' },
          item9: { name: 'Item 9' },
          item10: { name: 'Item 10' },
        },
        cols: 2
    },
    computed: {
          columns: function() {
              let columns = [];
              let mid = Math.ceil(Object.keys(this.items).length / this.cols);

              for (let col = 0; col < this.cols; col++) {
                  columns.push(Object.entries(this.items).slice(col * mid, col * mid + mid).map(entry => entry[1]));
              }
              return columns;
          }
      }
});
.container {
  display: flex;
  border: 1px solid;
}
.col {
  margin: 10px;
  border: 1px solid;
  flex-grow: 1;
  display: flex;
  flex-direction: column;
}
.item-container {
  border: 1px solid;
  padding: 5px;
  margin: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
  <main>
    <p><label>Columns:<label> <input type="number" v-model="cols"></p>
    <div class="container">
      <div class="col" v-for="column in columns">
        <div class="item-container" v-for="(item, index) in Object.keys(column)">
        {{column[item].name}}</div>
      </div>
    </div>
</main>

Related