Splitting a list of elements into columns

I have a page with a list of items, eg:

<div class="post">1</div>
<div class="post">2</div>
<div class="post">3</div>
<div class="post">4</div>
<div class="post">5</div>

I’d like to split this into multiple columns using javascript, distributing the items one column at a time, eg:

<div class="col">
 <div class="post">1</div>
 <div class="post">3</div>
 <div class="post">5</div>
</div>
<div class="col">
 <div class="post">2</div>
 <div class="post">4</div>
</div>

(It can’t be done server-side as the number of columns will change dynamically depending on the viewport width)

Since the number of columns will vary, ideally I’ll need a function where I can specify the number of columns.

Javascript isn’t my speciality so can someone point me in the right direction please? I’m using jQuery but vanilla JS is fine too. Thanks!

Just a very rough run up at this. Possibly missing the simple solution

edit: Bit of a cleanup

const outerHTML = elem => elem.outerHTML

// example when used with an array ['a', 'b', 'c', 'd', 'e']
// and numCols of 2,  returns [['a',' c', 'e'],['b', 'd']]
const sortIntoColumns = (numCols) =>
  (columns, value, i) => {
    const index = i % numCols

    columns[index] = Array.isArray(columns[index])
      ? [...columns[index], value]
      : [value]

    return columns
  }

const concatHTML = (html, column) =>
  `${html}<div class='col'>\n${column.join('\n')}\n</div>\n`


const toColumns = (elems, numCols = 1) =>
  Array.from(elems)
    .map(outerHTML)
    .reduce(sortIntoColumns(numCols), [])
    .reduce(concatHTML, '')

const posts = document.querySelectorAll('.post')
console.log(toColumns(posts, 2))

/*
<div class='col'>
<div class="post">1</div>
<div class="post">3</div>
<div class="post">5</div>
</div>
<div class='col'>
<div class="post">2</div>
<div class="post">4</div>
</div>
*/

Based on something like

<div id='posts'>
  <div class="post">1</div>
  <div class="post">2</div>
  <div class="post">3</div>
  <div class="post">4</div>
  <div class="post">5</div>
</div>
// Usage
const posts = document.querySelector('#posts')
const postElems = document.querySelectorAll('.post', posts)

posts.innerHTML = toColumns(postElems, 2)

codepen link here

Perfect, thanks! That’s working though I had to change this:

.reduce(sortIntoColumns(2), [])

to:

.reduce(sortIntoColumns(numCols), [])

Now I’m going to work out exactly what this code does :slight_smile:

Can’t say i like the idea of orphaning all those arrays into garbage cleanup.
(All major browsers except IE, and Opera Mini.)

//Define number of columns.
const x = 3;
//Create cols, store for later use.
var cols = [];
for(i = 0; i < x; i++) {
  var c = document.createElement("div");
  c.className = "col";
  document.body.appendChild(c);
  cols.push(c);
}
//Move posts to cols.
document.querySelectorAll('.post').forEach((post,index) => { cols[index % x].appendChild(post); })

Sorry you lost me?

Specifically? Array.from(elems)?

Every time you add a value to the array, you orphan the existing array and create a new one.

I must admit my head is a bit sketchy right now (A few festive whiskeys), but it is a standard pattern in functional programming as far as I am aware. Avoiding mutation.

You mean as opposed to just using push?

1 Like

The array was entirely encased within the function, there was no need to avoid mutation.

IMO, you’re over-applying the pattern. But, both codes work, so… swings and roundabouts, we both just have different ways of writing the code.

1 Like

I think you are spot on there. probably best I walk away from the keyboard now…

Point taken m_hutley, it is food for thought.

Nice one, thanks! It’s certainly a more concise solution and I understand it better (probably as it’s closer to how I’d do it with PHP) so I’ll have a play.

Thanks both - much appreciated!

Just thought I would throw this in here.
But why use javascript (or PHP) for this, when CSS is equipped to handle layout?

On another side-note. Semantically, lists are marked up with <li> tags, not <div>

5 Likes

I think SamA74’s is most likely the accepted answer

That said, due to it bugging me here is a revision to my code using m_hutley’s approach of creating DOM elements.

Maybe easier to reason with

function createElement (tagName, className = '') {
  const elem = document.createElement(tagName)

  elem.className = className
  return elem
}

function columnsFrom (elements, numColumns) {

  const fragment = new DocumentFragment()

  elements.forEach((element, i) => {

    const column = i % numColumns
    const children = fragment.children

    if (!children[column]) fragment.appendChild(createElement('ul', 'col'))

    children[column].appendChild(element)
  })

  return fragment
}

Usage example

const post = document.querySelector('#post')
const postItems = document.querySelectorAll('.item', post)
const columns = columnsFrom(postItems, 3)

post.innerHTML = ''
post.appendChild(columns)

Codepen updated

You’re quite right and ordinarily I would but I need to be able to manipulate the individual columns.

Thanks again! I’ll take a proper look after Christmas :slight_smile:

Although css columns will create columns as necessary I believe that the criteria for the question was for the divs to run in a horizontal sequence. Css columns however will stack the divs in each column in a vertical order. e.g. column1 would be 1,2,3,4 and column 2 would be 5,6,7,8 and so on. Instead of column1 = divs 1,3,5,7 and column2 = divs 2,4,6,8 etc.

To have the div numbers going horizontal and then forming columns as they wrap will require the CSS Grid layout method using the proposed value of 'masonry’. Unfortunately that value is not implemented fully yet so we will have to wait a while before production use.

Of course this assumes that each div.post is a different height otherwise flex or grid could do it now if all posts were the same height (which I doubt is the case).

1 Like

Yeah, they are indeed different heights and it’s a masonry-style layout we’re going for. Good point about the columns and as you say, CSS grid is probably the closest pure-CSS solution.

Changing the layout like this affects accessibility so we might need to look at setting the tab order too.

1 Like

I see that now, though it did not come across in my initial read.

To me, this layout would give a different visual flow to the actual content flow. Varying heights along a row, arranged in columns, to me would suggest reading in a column flow, vertical direction (as per columns). But the actual document content would be in a row flow (line by line), horizontal direction.
How a person would visually read it, differs from how a bot/screen-reader would read it.
Though I think “masonry” scripts already exist, for things like galleries.

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.