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.
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.
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.
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 .
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.
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 .
Edit:
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 :
@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 JavaScriptEdit: 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>
display:inline-block
which have the intended behaviorCSS grid layout
and you can use css columns or vertical flex
s instead but the elements would be aligned from top to bottom changing the whole layout.That was for the css, for the JavaScript:
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>
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With