Reputation: 774
I have a scenario where I have to create a MxN grid dynamically keeping the grids square, the code for that situation is here
rerender = (event) => {
const height = document.getElementById("y-input").value;
const width = document.getElementById("x-input").value;
// console.log(`${event.target.id} :: ${event.target.value}`);
console.log(`${height} :: ${width}`);
const cellContainer = document.getElementById("cell-container");
cellContainer.style.gridTemplateRows = `repeat(${height}, 1fr)`;
cellContainer.style.gridTemplateColumns = `repeat(${width}, 1fr)`;
cellContainer.innerHTML = "";
[...Array(height * width).keys()]
.map(() => document.createElement('div'))
.map((e) => {
e.className = "cell";
return e
})
.map((e) => cellContainer.appendChild(e))
}
#grid-container {
width: 500px;
height: 500px;
background-color: aqua;
padding: 8px;
}
#cell-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.cell {
background-color: blue;
min-width: 4px;
min-height: 4px;
margin: 1px;
aspect-ratio: 1/1;
}
<div>
<label for="x-input">width</label>
<input value=2 min=1 max=50 type="number" name="x" id="x-input" style="width: 4ch;" onchange="rerender(event)">
<label for="y-input">height</label>
<input value=2 min=1 max=50 type="number" name="y" id="y-input" style="width: 4ch;" onchange="rerender(event)">
</div>
<div id="grid-container">
<div id="cell-container">
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
</div>
</div>
I am using the grid layout where number of rows and columns are being changed dynamically by this
cellContainer.style.gridTemplateRows = `repeat(${height}, 1fr)`;
cellContainer.style.gridTemplateColumns = `repeat(${width}, 1fr)`;
and using the aspect-ratio
property to keep the cells square. The code works perfectly when we are drawing the square(height and width are same) and in a rectangular case where width is greater than height.
The rectangular case where height is greater than width is also working but in that case there is y-overflow, how can I prevent that overflow ?
https://codesandbox.io/s/wild-violet-kk6h9
Upvotes: 3
Views: 287
Reputation: 6692
Since everything is a square, we can use fake rows and columns to size the cells properly. Solution at the end of this post.
If I understand your problem correctly, you want
W
×H
cells gridN
×N
pixels squareSo in the case that you got working, say for example, W=3; H=2
, we get a result that looks like this:
I assume that we're looking for a pure-CSS solution, since a JS solution would be both trivial and suboptimal. So before we proceed, let's modify the JS to just give us CSS custom properties and then we can reason in CSS only:
--per-row
, the number of cells in 1 row,--per-col
, the number of cells in 1 column.grid.style.setProperty('--per-row', width);
grid.style.setProperty('--per-col', height);
#grid {
--per-row: 2;
--per-col: 2;
grid-template-columns: repeat(var(--per-row), 1fr);
grid-template-rows: repeat(var(--per-col), 1fr);
}
PS: in every code snippet, I'll omit the lines that aren't relevant to the current discussion.
Now what doesn't work in your solution is when H > W
, so let's take an example where W=2; H=3
. We want our grid to look like this:
But since the parent (cyan blue box) is also a square, we can see that this example and the previous one have the same resulting cell dimensions. In the case of W=3; H=2
we kind of see an empty line at the bottom; and in the case of W=2; H=3
we kind of see an empty column on the right. In both cases, we lay out our cells in a 3×3
grid! This is simply max(width, height)
:
So let's create this grid with CSS max():
#grid {
--per-row: 2;
--per-col: 2;
--max: max(var(--per-row), var(--per-col));
grid-template-columns: repeat(var(--max), 1fr);
grid-template-rows: repeat(var(--max), 1fr);
}
However, with this grid template, we'll always create 3 columns in both the 3×2
case and the 2×3
case. It's going to work great for W > H
but the H > W
will look wrong. We need to refactor how the columns template is calculated.
First, the number of column we actually want. That's the easy part, it's --per-row
:
grid-template-columns: repeat(var(--per-row), 1fr);
But now our 2×3
grid looks like this because each column is 1fr
, which results in 50%
of the full width:
Now, we need to find the proper width of a column. We've seen that using 1fr
doesn't work, but since everything is a square, we know that in both the 3×2
case and the 2×3
case we need each column to be ⅓ of the width, or 100% / 3
, or calc(100% / var(--max))
:
grid-template-columns: repeat(var(--per-row), calc(100% / var(--max)));
We now have a layout that will work for any W
and H
with two simple lines of CSS:
#grid {
grid-template-columns: repeat(var(--per-row), calc(100% / var(--max)));
grid-template-rows: repeat(var(--max), 1fr);
}
Because of how "vertical" layout works (technically, "block direction" in CSS flow layout), we can simplify how rows are created. grid-template-rows
will create a fixed number of rows, but now that we have a strong definition for the number of columns in grid-template-columns
, we can just say "create as many rows as needed to fit all of the children", which is exactly what grid-auto-rows does:
grid-auto-rows: calc(100% / var(--max));
This uses the same math as for the width of the columns, since everything here is a square.
Because support for CSS max()
still isn't great, this is something we can do in JS without any performance issue:
grid.style.setProperty('--per-row', width);
grid.style.setProperty('--per-col', height);
grid.style.setProperty('--max', Math.max(width, height));
You'll notice that at this point, we aren't using --per-col
anymore thanks to grid-auto-rows
, so we can remove it:
grid.style.setProperty('--per-row', width);
grid.style.setProperty('--max', Math.max(width, height));
#grid {
--per-row: 2;
--max: 2;
}
Here's your initial code, with the modifications explained in my answer.
rerender = (event) => {
const height = document.getElementById("y-input").value;
const width = document.getElementById("x-input").value;
const grid = document.getElementById("grid");
grid.style.setProperty('--per-row', width);
grid.style.setProperty('--max', Math.max(width, height));
grid.innerHTML = "";
[...Array(height * width).keys()]
.forEach(() => {
const e = document.createElement('div')
e.className = "cell";
grid.appendChild(e)
})
}
#container {
width: 500px;
height: 500px;
background-color: aqua;
padding: 8px;
}
#grid {
display: grid;
height: 100%;
--per-row: 2;
--max: 2;
grid-template-columns: repeat(var(--per-row), calc(100% / var(--max)));
grid-auto-rows: calc(100% / var(--max));
gap: 2px;
}
.cell {
background-color: blue;
aspect-ratio: 1/1;
}
<div>
<label for="x-input">width</label>
<input value=2 min=1 max=50 type="number" name="x" id="x-input" style="width: 4ch;" onchange="rerender(event)">
<label for="y-input">height</label>
<input value=2 min=1 max=50 type="number" name="y" id="y-input" style="width: 4ch;" onchange="rerender(event)">
</div>
<div id="container">
<div id="grid">
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
</div>
</div>
Upvotes: 4