flex space-between CSS(SCSS)だけの均等配置で最終行を左揃え

#space-betweenの均等配置で最終行を左揃え

##はじめに

flexboxのjustify-contentプロパティには両端に合わせて均等に配置してくれる便利機能がある。
ただこの機能には、最後の行のアイテムが少なくても両端揃えになってしまうという問題がある。

対処法としてはbefore,after属性で疑似的に要素を追加し両端揃えにする方法や、
jsでelementを数えてmarginを入れるパターンなどがあるが、
before,after属性が使えないやわざわざjsを読まなければという面倒さが残ってしまう。

これらを解消するため、SCSSで簡単に均等配列＆最終行を左揃えにできる機能を作った。

##使用言語

- HTML
- SCSS (Stylusなどでも可)

##実装

<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="result" data-user="k1-web" data-slug-hash="KKKOWrE" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="KKKOWrE">
  <span>See the Pen <a href="https://codepen.io/k1-web/pen/KKKOWrE">
  KKKOWrE</a> by k1-web (<a href="https://codepen.io/k1-web">@k1-web</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>



##詳細

```.html
<ul class="list">
    <li>item</li>
    ~~~
    <li>item</li>
</ul>
```
簡単なDOMを生成、ulタグにdisplay:flex;をあててliタグを配置する感じにします。
必要な情報としては、ulタグの全体の横幅と、liタグのアイテムの横幅のふたつで、
仕様上liタグの横幅が可変だと、marginがぶれるので横幅を確定させたものでないとうまくいきません。


```.scss
.list {
    $outwidth: 900px;//親要素の幅
    $itemwidth: 200px;//アイテム要素の幅
    $num: floor( $outwidth / $itemwidth );//要素の数

    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;    
    width: $outwidth;    
    > li {
        display: inline-block;
        width: $itemwidth;
        height: 120px;
        background-color: #eee;
        &:nth-child(n+#{$num+1}) {
            margin-top: 10px;
        }
        &:not(:nth-child(#{$num}n)):last-child {
            margin-right: auto;
        }
        @for $i from 1 through $num - 2 {
            &:nth-child(#{$num}n-#{$i}) {
                @for $j from 1 through $i {
                    &:nth-last-child(#{$j}) {
                        margin-left: calc( (100% - #{$itemwidth} * #{$num}) / #{$num - 1} );
                    }
                }
            }
        }
    }
}
```
SCSSの全体像。ひとつずつ解説。

###解説1

```.scss
.list {
    $outwidth: 900px;//親要素の幅
    $itemwidth: 200px;//アイテム要素の幅
    $num: floor( $outwidth / $itemwidth );//要素の数

    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;    
    width: $outwidth;
}
```
必要な入力値は```$outwidth```と```$itemwidth```、前述したとおりの全体の横幅とアイテムの横幅です。
その下の```$num```という変数は```$outwidth```の中に```$itemwidth```がいくつはいるかを計算しています。
単純に割り算をしても小数点が出てしまうので、SCSS機能のfloor()を使用しています。

また必要なflexプロパティはjustify-content: space-between;（両端揃え）とflex-wrap: wrap;（折り返し）です。


###解説2
```.scss
.list {
    > li {
        display: inline-block;
        width: $itemwidth;
        height: 120px;
        background-color: #eee;
        &:nth-child(n+#{$num+1}) {
            margin-top: 10px;
        }
        &:not(:nth-child(#{$num}n)):last-child {
            margin-right: auto;
        }
    }
}
```
アイテムの横幅に先程の変数をあてています。
```:nth-child(n+#{$num+1})```は項目としては任意です。これは2行目以降に余白を作っています。
わざわざクラスを当てなくともアイテムの数に応じて自動的に余白を指定できます。

次に、```:not(:nth-child(#{$num}n)):last-child```この項目は、
「一番最後のアイテム」かつ「1行のアイテムの数と最終行のアイテムの数が違う」
という条件、最終行を左揃えにしたいときに機能します。

margin-right: auto;を最後のアイテムに付与すると、
space-betweenが解除されて余白を消して左揃えに並ぶという仕様を利用しています。

###解説3

```.scss
.list {
    > li {
        @for $i from 1 through $num - 2 {
            &:nth-child(#{$num}n-#{$i}) {
                @for $j from 1 through $i {
                    &:nth-last-child(#{$j}) {
                        margin-left: calc( (100% - #{$itemwidth} * #{$num}) / #{$num - 1} );
                    }
                }
            }
        }
    }
}
```
余白間なしで左揃えしている状態に、余白を計算して付け加えます。
これは2か所の@forで形成されていて、まずはそこから解説します。

```.scss
@for $i from 1 through $num - 2 {
    &:nth-child(#{$num}n-#{$i}) {
```
最終行のアイテムの数が「1」または「1行のアイテムの数と同じ」という条件を除いた分をまわしています。

最終行のアイテムの数が「1」の場合は、margin-right: auto;を最後のアイテムに付与していると、そもそも余白が必要なく、
最終行のアイテムの数が「1行のアイテムの数と同じ」場合は、自動的に均等配置になるので必要ありません。

$numが「5」だった場合、:nth-childを計算すると、throughが3となり
(5n - 1)(5n - 2)(5n - 3)と最後のアイテムから1~3つ前にあるアイテムにスタイルがあたります。

```.scss
@for $j from 1 through $i {
    &:nth-last-child(#{$j}) {
```
(5n - 1)は左から4番目のアイテムで、margin-leftが必要なのは:nth-last-child(1)のときだけ、
(5n - 2)は左から3番目のアイテムで、margin-leftが必要なのは:nth-last-child(1),(2)のふたつ、
などのように最後から何番目のアイテムかを判定してmargin-leftをあてることができます。

```.scss
margin-left: calc( (100% - #{$itemwidth} * #{$num}) / #{$num - 1} );
```
そして、margin-leftの算出方法はcalc()関数を使用ます。
```(100% - #{$itemwidth} * #{$num})```は、横幅100%からアイテムの横幅×個数分を引いた余白合計
そこから余白の数の分母を割ってひとつの余白あたりのmargin-leftの値を算出します。

##あとがき
似たような実装は探せばいろいろあると思います。
ロジックさえ作ればJSなんていらないよーってのが分かっていただけたら嬉しいです。