Pinterest風なGridレイアウトをforeachの順序を崩さずに実現するまで【JS｜意外とかんたん】

## 今回実現すること

フロントエンドをされている方なら１度は憧れる（？）
Pinterest風なGridレイアウトを
ライブラリを使わず、かんたんなJSで実現したので
過程と解説を書いていきたいと思います。

既に個人開発でつくったWebアプリには実装していて
問題なく動いてます。

実際に実装した画面がこちらです。
![cheersbox.herokuapp.com_(iPhone X).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560551/644845f6-87d7-27a7-95c9-4b1cc7d40b43.png)

[チャットライクな寄書きWebサービス Cheers Box](https://cheersbox.herokuapp.com/)

もし実装する機会があれば、参考にしていただけれ幸いです。


## まずは調査しました

今回、Pinterest風なGridレイアウトを作る上で、
**foreachの順序を崩さずに、画面上から下へcreated_atの降順になるようにする**
という仕様がありました。

順序を表すとこう言う感じです。
**① ②**
**③ ④**
**⑤ ⑥**


### まず見つけた記事
[【WordPress】cssの追記のみでPinterest風のグリッドデザイン](https://yusukeito.co/wordpress-css-likepinterest-grid-design/)
順序はこのようになってしまいます。
**① ④**
**② ⑤**
**③ ⑥**


### 次に見つけた記事
[Masonryレイアウトをたった3行のシンプルなCSS Gridで簡単に実装できるようになります](https://coliss.com/articles/build-websites/operation/css/css-masonry-layouts-in-firefox.html)
めちゃくちゃ簡単そうですが
まだブラウザ互換的に使い物になりません。


### ライブラリも発見
Pinterest風なGridレイアウトを簡単に作れるライブラリもあるようです。
[driveway｜http://jh3y.github.io/driveway/](http://jh3y.github.io/driveway/)
[Masonry｜https://masonry.desandro.com/index.html](https://masonry.desandro.com/index.html)
しかし
「たかだか１画面のレイアウトのためにライブラリいれるのもなあ…」
ということで**自作することにしました**


## Pinterest風なGridレイアウトの原理

自作する前に、Pinterestが実際どのように実現しているかを調べてみました。
![Screen Shot 2021-05-07 at 17.56.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560551/57151912-ca95-2dbd-141e-b8261aaa1b45.png)

ソースコードを見るとおそらく
どうにかして**JSで各要素にstyleを書き込んでいる**ということが予想できます。

そこで、**それを真似することにしました。**

やることは、
**要素のサイズや要素同士の余白計算して、
計算結果を次の要素の`style transform`に設定する**
ということだけです。

いたってシンプルですね。


## コードと解説
簡易的に`html`をこういうものだとします。(実際のWebアプリには`html`使ってませんが)

```index.html
<div class="card-wrapper">  <!-- Gridレイアウトしたい親要素 -->
	<div class="card">  <!-- Gridレイアウトしたい要素 -->
		<img src="img01.png">
	</div>
	<div class="cardr">
		<img src="img02.png">
	</div>
	<div class="card">
		<img src="img03.png">
	</div>
	<div class="card">
		<img src="img04.png">
	</div>
	<div class="card-">
		<img src="img05.png">
	</div>
	<div class="card">
		<img src="img06.png">
	</div>
</div>
```

今回Pinterest風なGridレイアウトで表示したい要素には
画像が含まれています。


そこでまずは、画像が全て読み込まれたかどうかを判定します。

```index.js
const images = document.querySelectorAll('img');

let loadCnt = 0;

if (images.length > 0) {
	for (const img of images) {
		let image = new Image();
		image.src = img.src;
		image.addEventListener('load', () => {
			loadCnt++;
			if (loadCnt == images.length) {  // 画像が全部読み込まれたか判定しています
				makePageCardGridLayout();  // この後、このメソッドの中身を書きます
			}
		})
	}
}
```
ここまでで、ページないの画像全て読み込んだら、`makePageCardGridLayout()` というメソッドが呼ばれるところまできました。

次に`makePageCardGridLayout()` でPinterest風なGridレイアウトを実現していきます。

```index.js
makePageCardGridLayout() {
		
	const cards = document.getElementsByClassName('card');

	if(cards.length > 0) {
		let evenPosX = 0;
		let evenPosY = 0;
		let oddPosY = 0;
		let gridWindow;
		let sh;
	
		for (let i = 0; i < cards.length; i++) {
			const oddPosX = cards[0].clientWidth;
			i == 0 ? gridWindow = cards[0].closest('.card-wrapper') : i; // Gridの親要素を取得します

			if (i % 2 == 0) { // Gridに配置される偶数番目の要素に対する処理
				cards[i].setAttribute("style", "transform: translateX("+ evenPosX +"px) translateY(" + evenPosY + "px)");　 // スタイルに配置したい位置を記述します
				evenPosY = evenPosY + cards[i].clientHeight;　 // 今のY軸の位置に要素の高さを加算
			} else if (i % 2 != 0) {　 // Gridに配置される奇数番目の要素に対する処理
				cards[i].setAttribute("style", "transform: translateX(" + oddPosX + "px) translateY(" + oddPosY + "px)");　 // スタイルに配置したい位置を記述します
				oddPosY = oddPosY + cards[i].clientHeight;　 // 今のY軸の位置に要素の高さを加算
			}
		}
		evenPosY > oddPosY ? sh = evenPosY + 150 : sh = oddPosY + 150;　// 全部の要素が配置し終わったら、親要素の高さを設定するために計算します
		gridWindow.style.height = sh+'px';　// 計算した高さを親要素に指定します
	}
}
```

このように配置したい要素(`.card`)を取得して、**forで1つずつ配置する場所を指定**してあげています。
（なんで`for`なのかは覚えていません。笑　`for of`でもいけるはずです）
**① ②**
**③ ④**
**⑤ ⑥**
**奇数番目の要素は、1つ前の奇数番目の要素の位置とサイズから配置位置を計算**
**偶数番目の要素も、1つ前の偶数番目の要素の位置とサイズから配置位置を計算**しています。

`transform: translate`で子要素の位置を指定しているので、
**親要素の`.card-wrapper` は高さが０の状態**になっています。

そこで最後に、**奇数番目か偶数番目で下側にある要素から、150px加算して親要素の高さに指定しています。**

コードは以上です。

レスポンシブを実現するのも、
画面サイズによって、`makePageCardGridLayout()`を流用したメソッドを呼び出せば
スムーズに実現できると思います。


## 結果、意外とかんたんにできた
「え、もうおわり？」
「思ったよりコード量少ないな」

と思われた方も多いんじゃないでしょうか。

実はこちらのWebアプリには、ライブラリを使用せずに
JSで実装した挙動が他にもいくつかあります。
[チャットライクな寄書きWebサービス Cheers Box](https://cheersbox.herokuapp.com/)

たとえば、

- 寄書き作成時に、生のJSなのにSPAのようなプレビュー機能がある
- 寄書きは、メッセージがチャットのように1つずつ時間差で表示される
- 下記の画像下部の部分はメニューで、画面全体がメニュー連動のカルーセルになっている

![cheersbox.herokuapp.com_(iPhone X).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560551/644845f6-87d7-27a7-95c9-4b1cc7d40b43.png)
などです。

それらもなるべくシンプルに実装してみたので
また実装方法を記事にできたらと思います。

より良い実装、おすすめのライブラリがあれば、ぜひご教授ください。

最後までご覧いただいてありがとうございました😊