Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Align arbitrary number of elements with different widths to a grid with wrapping

EDIT

I've accepted the answer given by @user943702 below. I needed to modify it slightly to work with my Vue implementation as shown in the snippet below.

const theElements = [{
  name: "ele1",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }, {
    name: 4
  }, {
    name: 5
  }]
}, {
  name: "ele2",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele3",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele4",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele5",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele6",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele7",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele8",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele9",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele10",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele11",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }, {
    name: 4
  }, {
    name: 5
  }]
}, {
  name: "ele12",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}];

new Vue({
  el: '#ele-grid',
  data: {
    elements: theElements
  },
  methods: {
    // find the first grid line excess {max}
    // return index; -1 means no overflow
    firstoverflowline: function(cols, max) {
      var sum = 0;
      for (var i = 0; i<cols.length; ++i) {
        sum += cols[i];
        if (sum >= max)
          return i;
      }
      return -1;
    },
    // compute max no of columns in grid
    // use by `grid-template-columns:repeat(<max>, max-content)`
    computegridlines: function(container) {
      var cols = getComputedStyle(container).gridTemplateColumns.split(/\s+/).map(parseFloat);
      var x = this.firstoverflowline(cols, parseFloat(getComputedStyle(container).width));
      if (x == -1) return;
      container.style.gridTemplateColumns = `repeat(${x}, max-content)`;
      this.computegridlines(container);
    },
    // polyfill `width:max-content`
    maxcontent: function(container) {
      var items = Array.from(container.children);
      for(var i = 0; i < items.length; i++) {
      	var item = items[i];
        item.style.display = "flex";
        item.style.flexFlow = "column";
        item.style.alignItems = "start";
        var max = Array.from(item.children).reduce(function(max,item) {
          var {left, right} = item.getBoundingClientRect();
          return Math.max(max, right - left);
        }, 0);
        item.style.width = `${max}px`;
      }
    },
    // flex-grid-ify a container
    flexgrid: function(container) {
      container.style.display = `grid`;
      container.style.gridTemplateColumns = `repeat(${container.children.length}, max-content)`;
      this.computegridlines(container);
      this.maxcontent(container);
    }
  },
  mounted: function() {
    var container = document.getElementById('ele-grid');
    var _this = this;
    this.flexgrid(container);
    window.onresize = function(e) { _this.flexgrid(container); }
  }
});
#ele-grid {
  width:100vw;
}

.ele-card {
  border: 1px solid black;
  background: cyan;
  margin: 5px 3px;
}
.ele-card .children {
  display: flex;
  flex-wrap: nowrap;
  padding: 5px;
}
.ele-card .child {
  margin: 0 5px;
  width: 30px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  border: 1px solid black;
  background: magenta;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.min.js"></script>
<div id="ele-grid">
  <div class="ele-card" v-for="ele in elements" :key="ele.name">
      <div class="element">{{ele.name}}</div>
      <div class="children">
        <div class="child" v-for="child in ele.children" :key="child.name">{{child.name}}</div>
      </div>
  </div>
</div>

I have an unknown number of elements that can have different widths. I want to align these elements in a grid so that their left sides line up in each column. Additionally, I want the elements to wrap when the window is sized smaller and maintain the grid. I mocked up what I want in the images below.

enter image description here

enter image description here

I am using VueJS 2 to populate the elements into the grid and CSS Flexbox to organize the elements using the following CSS. Below is an example snippet of how it functions now:

const theElements = [{
  name: "ele1",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }, {
    name: 4
  }, {
    name: 5
  }]
}, {
  name: "ele2",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele3",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele4",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele5",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele6",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele7",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele8",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele9",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele10",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}, {
  name: "ele11",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }, {
    name: 4
  }, {
    name: 5
  }]
}, {
  name: "ele12",
  children: [{
    name: 1
  }, {
    name: 2
  }, {
    name: 3
  }]
}];

new Vue({
  el: '#ele-grid',
  data: {
    elements: theElements
  }
});
#ele-grid {
  display: flex;
  flex-wrap: wrap;
}
.ele-card {
  border: 1px solid black;
  background: cyan;
  margin: 5px 3px;
}
.ele-card .children {
  display: flex;
  flex-wrap: nowrap;
  padding: 5px;
}
.ele-card .child {
  margin: 0 5px;
  width: 30px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  border: 1px solid black;
  background: magenta;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.min.js"></script>
<div id="ele-grid">
  <div class="ele-card" v-for="ele in elements" :key="ele.name">
    <div class="element">{{ele.name}}</div>
    <div class="children">
      <div class="child" v-for="child in ele.children" :key="child.name">{{child.name}}</div>
    </div>
  </div>
</div>

This almost works; the elements each have their own width and wrap when the window is resized. However, the elements do not align to a grid.

I've also looked into using CSS Grid, but it looks like you either have to specify the width of each element or the number of columns, both of which I need to be arbitrary.

I'm open to any solution using CSS or JavaScript (not JQuery please). I'd prefer to not include a 3rd party library but will consider it if it's the only option.

like image 227
sorayadragon Avatar asked Dec 06 '17 14:12

sorayadragon


People also ask

Which CSS rule will you use to place a grid item into the grid so that it starts from the second column and second row and spans three columns and two rows?

Auto-placement by column Using the property grid-auto-flow with a value of column . In this case grid will add items in rows that you have defined using grid-template-rows . When it fills up a column it will move onto the next explicit column, or create a new column track in the implicit grid.

How do you align items inside the grid?

To align the item horizontally within the grid, we use the justify-content property and set it to center . With justify-content we can align the columns start , end , stretch or center .

What is grid wrapper?

The grid wrapper pattern is useful for aligning grid content within a central wrapper, while also allowing items to break out and align to the edge of the containing element or page when desired.

How do you arrange a grid element in CSS?

With CSS grid layout, the grid itself within its container as well as grid items can be positioned with the following 6 properties: justify-items , align-items , justify-content , align-content , justify-self , and align-self .


2 Answers

Edit:

  • As @user943702 has pointed out we can make use of max-content property, to remove the extraneous spaces in each column (do not confuse this property with that coming in the explanation though which is a widths value per element basis, and this one is per column basis)
  • For space distribution : there is a handy property called justify-content I've chosen to set it to center, among other values, you can set it to :

    space-between; /* The first item is flush with the start,the last is flush with the end */
    space-around;  /* Items have a half-size space on either end */
    space-evenly;  /* Items have equal space around them */
    stretch;       /* Stretch 'auto'-sized items to fitthe container */
    

Before getting to the script, there are a couple of notes :

  • You can set it to responsively changing using only css by: @media query one for each width and be done with it however the individual elements have an arbitrary width too so I'm gonna use JavaScript

Edit: Here is a script using the CSS media query method notice that the more you try to customize it to different device widths the more you risk to be caught when individual elements width changes unexpectedly.

const theElements = [{  name: "ele1",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }, {    name: 4  }, {    name: 5  }]}, {  name: "ele2",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele3",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele4",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele5",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele6",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele7",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele8",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele9",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele10",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele11",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }, {    name: 4  }, {    name: 5  }]}, {  name: "ele12",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}];

new Vue({
  el: '#ele-grid',
  data: {
    elements: theElements
  }
});
@media (min-width: 1020px) {
	#ele-grid {
		display:grid;
		grid-template-columns:repeat(5, 1fr); 	
        justify-content: center;
	}
}
@media (min-width:400px) and (max-width: 1020px) {
	#ele-grid {
		display:grid;
		grid-template-columns:max-content max-content max-content; 	
	}
}
@media (max-width: 400px) {
	#ele-grid {
		display:grid;
		grid-template-columns:max-content; 	
	}
}
.ele-card {
  margin: 5px 3px;
}
.ele-card .children {
  display: flex;
  flex-wrap: nowrap;
  padding: 5px;
}
.ele-card .child {
  margin: 0 5px;
  width: 30px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  border: 1px solid black;
  background: magenta;
}
.wrapper{
	border: 1px solid black;
  background: cyan;
  display:inline-block;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.28/vue.min.js"></script>

<div id="ele-grid">
  <div class="ele-card" v-for="ele in elements" :key="ele.name">
  	<div class="wrapper">
	    <div class="element">{{ele.name}}</div>
	    <div class="children">
	      <div class="child" v-for="child in ele.children" :key="child.name">{{child.name}}</div>
	    </div>
	  </div>
	</div>
</div>
  • To get the width as needed there is an excellent -moz-max-content property unfortunately it is not supported yet by the other browsers, so I've appended a child wrapper and make it display:inline-block which have the intended behavior
  • I'm using CSS grid layout and you can use css columns or vertical flexs instead but the elements would be aligned from top to bottom changing the whole layout.

That was for the css, for the JavaScript:

  • In a nutshell this scripts takes a layout with max columns (here 10 you can increase it), and see if it fits without scrolling, if not decrements.
  • In this script, elements are responsive using a the resize event.

const theElements = [{  name: "ele1",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }, {    name: 4  }, {    name: 5  }]}, {  name: "ele2",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele3",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele4",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele5",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele6",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele7",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele8",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele9",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele10",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele11",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }, {    name: 4  }, {    name: 5  }]}, {  name: "ele12",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}];

new Vue({
  el: '#ele-grid',
  data: {
    elements: theElements
  }
});
function resizeHandler(){
	colStart=10; 		// max number of columns to start with
	allCards= document.getElementsByClassName('wrapper');
	totalWidth=0;
	maxWidTab=[];
	for (i=colStart;i>0;i--){
		for(j=0;j<i; j++){									//initializing and resetting
			maxWidTab[j]=0;
		}
		for (j=0; j<allCards.length; j++){
			cellWidth=parseInt(getComputedStyle(allCards[j]).width);		//parseInt to remove the tailing px
			maxWidTab[j%i]<cellWidth?maxWidTab[j%i]=cellWidth:'nothing to be done';
		}
		for(j=0;j<i; j++){									//sum to see if fit
			totalWidth+=maxWidTab[j]+2+6		//borders and margins
		}
		if (totalWidth<innerWidth){
			grEl = document.getElementById("ele-grid");
			grEl.style.gridTemplateColumns="repeat("+i+", max-content)";
			/*console.log(i);*/
			break;
		}else{
				totalWidth=0;							//resetting
		}
	}
}
	window.addEventListener("resize",resizeHandler);
	document.addEventListener ("DOMContentLoaded",resizeHandler);
#ele-grid {
	display:grid;
    justify-content: center;
	grid-template-columns:repeat(10, max-content); 	/* starting by 10 columns*/
}
.ele-card {

  margin: 5px 3px;
}
.ele-card .children {
  display: flex;
  flex-wrap: nowrap;
  padding: 5px;
}
.ele-card .child {
  margin: 0 5px;
  width: 30px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  border: 1px solid black;
  background: magenta;
}
.wrapper{
	border: 1px solid black;
  background: cyan;
  display:inline-block;
}

</style>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.28/vue.min.js"></script>

<div id="ele-grid">
  <div class="ele-card" v-for="ele in elements" :key="ele.name">
  	<div class="wrapper">
	    <div class="element">{{ele.name}}</div>
	    <div class="children">
	      <div class="child" v-for="child in ele.children" :key="child.name">{{child.name}}</div>
	    </div>
	  </div>
	</div>
</div>
like image 60
user10089632 Avatar answered Sep 24 '22 15:09

user10089632


CSS grid-template-columns does support content-aware value which is max-content. The only question is that how many columns should be there.

I write an algorithm to probe maximum number of column. The implementation involves JS and requires browser to support CSS Grid. Demo can be found here. (I use Pug to create same source structure as yours and styling is also same as yours so that we can focus on JS panel, the implementation).

In demo, changing viewport size will re-flow grid items. You may trigger re-flow at other interesting moments manually by calling flexgrid(container), e.g. loading items asynchronously then re-flow. Changing dimension properties of items is allowed as long as source structure keeps unchanged.

Here's the algorithm

Step1) Set container as grid formatting context, layout all grid items in one row, set each column width to max-content

|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggg|hhh|iii|

Step2) find first overflow grid line

|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggg|hhh|iii|
                  ^overflowed

Step3) reduce grid-template-columns to, in our case, 3. Since grid-row default to auto, CSS engine layouts a grid item on next row when it goes beyond last column grid line. I called this "wrapping". In addition, grid items are auto expanded due to grid-template-columns:max-content(e.g. "ddd" is expanded to the length of widest content of first column)

|---container---|
|aaaaa|bbb|ccc|
|ddd  |eee|fff|
|ggggg|hhh|iii|

Since all column grid lines sit "inside" container, we have done. In some cases, a new overflowed grid line is being introduced after "wrapping", we need to repeat step2&3 until all grid lines sit "inside" container, e.g.

#layout in one row
|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggggg|hhhhh|iii|

#find the first overflowed grid line
|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggggg|hhhhh|iii|
                  ^overflowed

#reduce `grid-template-columns`
|---container---|
|aaaaa  |bbb  |ccc|
|ddd    |eee  |fff|
|ggggggg|hhhhh|iii|

#find the first overflowed grid line
|---container---|
|aaaaa  |bbb  |ccc|
|ddd    |eee  |fff|
|ggggggg|hhhhh|iii|
                  ^overflowed

#reduce `grid-template-columns`
|---container---|
|aaaaa  |bbb  |
|ccc    |ddd  |
|eee    |fff  |
|ggggggg|hhhhh|
|iii    |

#find the first overflowed grid line
#None, done.
like image 40
user943702 Avatar answered Sep 22 '22 15:09

user943702