ElmにクリーンでハックのないCSS modulesを


この記事はElmでCSS modulesを使う新しい方法を提案します。
また、ページの多言語対応などにも便利な `elm-html-with-context` の使い方も一緒に紹介します。

## CSS modules とは

CSS modules とは、CSSの再利用性を高める手法の1つです。
従来のCSSにはいわゆる「名前空間」が存在しません。
たとえばボタンに表示される文字の見た目を変える目的で `text` という名前のクラスに対してスタイルを宣言したとします。
別の日に入力欄に表示される入力文字列の見た目を変えようと思って、同じ `text` という名前のクラスを使ったとすると、入力欄にだけ適用しようと思った見た目の変化がボタンに表示される文字にも反映されてしまいます。

小さなプロジェクトであれば何ら問題ありませんが、プロジェクトが大きくなってくると「このクラス名は他で使われていないだろうか？」と細心の注意を払う必要があります。
特にCSSファイルが複数存在する場合、全ファイルをチェックしないといけないため、生産性が大きく低下してしまいます。
CSS modules を使うとこのような名前の衝突を防ぐことができます。

CSS modules がやることはとても単純で、CSSファイルを編集してクラス名に「識別子」をつけます。

たとえば `button.css` というCSSファイルの中で

```css
.text {
  color: red;
}
```

というスタイルが宣言されているとき、これを

```css
._text_xkpkl_5 {
  color: red;
}
```

のように自動的に書き換えてしまうのです。

もちろんCSSファイルのクラス名を書き換えるだけでは本来の `text` クラスを持つHTML要素にスタイルが適用されなくなってしまいますから、書き換えたクラス名の対応関係を JSON オブジェクトとして以下のように出力します。

```json
{
  "text": "_text_xkpkl_5",
  "background": "_background_xkpkl_10"
}
```

HTML側がこの JSON オブジェクトを使って適切なクラス名を各要素に付与するようにすることで、無事に CSS で定義したスタイルがその要素に適用されます。

このようなクラス名の衝突をなくす工夫によって、アトミックデザインなどに使える再利用可能な CSS の記述が可能になります。

## 関連手法との比較

「CSS modules を使いたいから Elm で CSS modules を使えるようにした」みたいなのは無能な人がやることです。
いったん落ち着いて「本当に Elm で CSS modules を使う新しい手法を考える必要があるのか」「他にクラス名の衝突を回避する方法はないのか」について考えましょう。

せっかちな方は [elm-css-modules-helper の使い方](#elm-css-modules-helper-の使い方) まで読み飛ばしてください。

### CSS in JS (elm-css) との比較

CSS modules とよく比較される手法として、CSS in JS と呼ばれるものがあります。
実際に Elm においても、[rtfeldman/elm-css](https://github.com/rtfeldman/elm-css) というツールが CSS in JS (この場合は CSS in Elm)の流儀を採用しています。

CSS in Elm である `elm-css` は、スタイルを Elm のプログラムとして記述し、インラインスタイルとして各要素に直接付与します。
インラインスタイルで付与するため、クラス名の衝突を気にする必要がありません。
ただし、`elm-css` には多くの課題が存在しており、特に日本の Elm コミュニティでは否定的な意見が多いです。

1. インラインスタイルを採用しているため、メディアクエリーや擬似クラスなど CSS の便利な機能が使えない
1. Elm プログラムとしてスタイルを記述するため、Sass や auto-prefixer、flexbugs-fixes などのツールチェーンが使えない
1. 型安全であることのメリットよりもデメリットが大きすぎる

    型安全性は1年に1度しか実行されないような処理にバグが残るのを防ぐことができる強力な手法ですが、
    スタイルについてはふつう全ての場合/全ての要素についてブラウザ上で目視で確認しながら進めるはずです。
    テキストエディタの補完によって簡単な typo は防げる上に、仮に typo があっても目視で確認しているときに気づけます。
    このようにメリットがほとんどないにも関わらず、「型安全にするため」という名目で独自のプロパティ名を逐一調べないといけません。

1. スタイルが Elm 内でしか共有できない

    Elm を段階的に導入しているプロジェクトを想像してください。
    一部を Elm で書いていて、他の部分を React や Vanilla JS で書いている場合、他の部分のスタイルと Elm で書かれている部分のスタイルを統一するためには、すでに書かれている CSS を Elm コードに修正したり、
    逆に Elm で書いたスタイルを CSS に書き直さなければいけません。

私自身もかつて `elm-css` の使い方を模索したことがあり、特に 1 と 2 の問題を解決するために [elm-css で PostCSS も使える CSS in Elm](https://qiita.com/arowM/items/ce20b08a65f03e2ec44b) という記事を書いたことがありますが、やはり 3 と 4 の問題は解決しなかった上に普通に CSS を書いたほうがシンプルだったため、現在では `elm-css` は使っていません。

CSS modules は基本的にただの CSS ファイルですから、名前衝突の問題を解消しつつ上記の全ての問題を解決することができます。

追記: elm-cssちゃんがいろいろ反省してその後全然違う感じになりました。（@miyamo_madokaさんのツイートを引用）
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">1のメディアクエリとかは今は使えるようになってるよ<br>2のツールチェーンはしょうがないね。自前で実装してくれ。変換関数作るだけだ<br>4のスタイル共有はやろうと思えばできるけどって感じ<br>elm-cssでは昔は生成してたんだけどやらなくなった。生成予定のstyle吐こうと思えば吐けるけど</p>&mdash; てんとう虫の大家（みやも） (@miyamo_madoka) <a href="https://twitter.com/miyamo_madoka/status/1339084454703185920?ref_src=twsrc%5Etfw">December 16, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

### BEM 記法との比較

BEM 記法もクラス名の衝突を解消するための手法です。
とてもシンプルな方針で、手作業で `block__element--modifier` みたいなキモいクラス名を一定のルールにしたがってつけるだけの手法です。
キモさとめんどくささ以外は特にデメリットがないので、無駄にごたごた手間がかかることをするコストと比較した場合に、このような運用によるカバーの方が勝る場面もあります。

今回ご紹介する `elm-css-modules-helper` を使った手法にもある程度の追加工数がかかるため、「手法のエレガントさ」にだまされずに客観的にどちらが良いかをよく考えた上で手法を選択してください。
私もあまり規模が大きくない場合は BEM 記法をもう少し簡素にした記法で対応することがあります。

### css-modules-loader との比較

さて、CSS modules を採用するにしても、実はすでに [cultureamp/elm-css-modules-loader](https://github.com/cultureamp/elm-css-modules-loader/) というライブラリが存在します。
素直にこのライブラリを使うのではいけないのでしょうか？

実は `elm-css-modules-loader` には多くの問題があります。
むしろこのライブラリにおいて正しいと言えるのは CSS modules を使おうと思った動機だけで他の全ての選択を間違っています。

冒頭で述べた CSS modules の機能のうち

* CSS ファイルを書き換えてクラス名に識別子を付与する
* 変換されたクラス名と、変換前のクラス名の対応を示す JSON を出力する

これらの部分は Elm とは全く関係ないので従来通りに webpack の `css-loader` に [`modules` オプション](https://github.com/webpack-contrib/css-loader#modules)を付与するなり、postcss の [`postcss-modules`](https://github.com/css-modules/postcss-modules)でJSONファイルを出力するなりすれば問題ありません。

重要なのは、どうやって Elm プログラム内で変換された後のクラス名を DOM に付与するかです。

`elm-css-modules-loader` は、Elm コンパイラーで生成された js コードを書き換えて、js コード内に存在する変換前のクラス名を変換後のクラス名に置き換える戦略をとっています。
このような Hacky な戦略を採用した結果

* `elm-hot-webpack-loader` がエラーを吐いて動かなくなる
* Elm コンパイラーの挙動が変わる度に動かなくなる

    実際に Elm 0.19 に対応するのに多くの作業が発生していたようですし、単純なバグフィックスでコンパイラーが新しくなっただけでも動かなくなる可能性があります。

などの具体的な問題が生じています。

さらにライブラリの API も正気の沙汰ではなく、Elm 側に事前に「CSS ファイルの中のこのクラス名を Elm 内で使います」と全部列挙しないといけません。
しかもその記法がすごいんです！

```elm
common :
    CssModules.Helpers
        { foo : String
        , bar : String
        , baz : String
        ...
        }
        msg
common =
    css "../../styles/common.scss"
        { foo = ""
        , bar = ""
        , baz = ""
        ...
        }
```

なんと、型レベルと値レベルの両方にクラス名を書かないといけません。

賢明な読者のみなさんの中には、「クラス名を列挙するのは、CSS 側にそのクラス名を定義しているのを忘れていないかチェックするためなのでは？」と思った方もいるかもしれません。
そんなことはありません。 CSS 側に存在しないクラス名を列挙してもコンパイルは通り、何の警告もされません。
型安全性もなにもなく、ただただ面倒な作業を強いられるだけなのです。
最高にクール！！

こんな面倒な列挙をしないでもよく、view 関数内で普通にクラス名を文字列で渡したらそのクラス名を自動的に変換後のものに置き換える設計だったら最高じゃないですか？
[arowM/elm-css-modules-helper](https://github.com/arowM/elm-css-modules-helper) を使えば、ハックなしでそんな夢が叶います！

![sakura-chan-under-the-bed.jpg](https://qiita-image-store.s3.amazonaws.com/0/16345/db4a73b7-5901-af62-8134-d4975e54aea4.jpeg)

## `elm-css-modules-helper` の使い方

### 全体の流れ

まず、[arowM/elm-css-modules-helper]((https://github.com/arowM/elm-css-modules-helper)) を使った CSS modules との連携について、大まかな流れをご紹介します。

1. クラス名の変換方法を示した JSON オブジェクトを生成する

    `webpack` を使っているのであれば [css-loader の modules オプション](https://github.com/webpack-contrib/css-loader#modules)をセットすれば、JS ファイル内で CSS ファイルを `require` した際にこの JSON オブジェクトを取得できます。
    `postcss` をお使いの方は `postcss-modules` で JSON ファイルを生成して、あとはなんかいい感じにがんばってください。

1. 取得した JSON オブジェクトを `flags` として Elm に渡す
1. `flags` として渡された JSON オブジェクトを `CssClass` に変換する
1. view 関数の中で付与したいクラス名を指定する

    `Html.Attributes.class` の代わりに `CssClass.class` を使うことで、内部的にそのクラス名を CSS modules によって変換された名前に変更します。

### サンプルプロジェクト `examples/simple`

では実際にサンプルプロジェクトの中身を見てみましょう。
[examples/simple](https://github.com/arowM/elm-css-modules-helper/tree/master/examples/simple) についてまず解説します。

#### クラス名の変換方法を示した JSON オブジェクトを生成する

```index.js
var Elm = require('./Main').Elm;
require('./styles/reset.scss');

var layout = require('./styles/layout.scss');
var input = require('./styles/input.scss');
var label = require('./styles/label.scss');
var app = require('./styles/app.scss');

```

`./styles/layout.scss` のような CSS ファイル (実際には SCSS) を読み込み、クラス名の変換結果が示された JSON オブジェクトをそれぞれ `layout` `input` `label` `app` などの変数に格納しています。

#### 取得した JSON オブジェクトを `flags` として Elm に渡す

```index.js
var wrapper = document.createElement('div');
document.body.appendChild(wrapper);
var app = Elm.Main.init({
  node: wrapper,
  // Pass CSS class name JSON object to Elm via flags.
  flags: {
    layout: layout,
    input: input,
    label: label,
    app: app,
  },
});
```

先ほど取得した JSON オブジェクトをまとめて `flags` として Elm に渡しています。

#### `flags` として渡された JSON オブジェクトを `CssClass` に変換する

`flags` として与えられた以下のオブジェクトに対応する型 `Style` を宣言し、Model に保持できるようにしておきます。

```json
{
  layout: layout,
  input: input,
  label: label,
  app: app,
}
```

```Main.elm
type alias Model =
    { style : Style
    , ...
    , ...
    }


{-| This type represents dictionaries for CSS class names generated by CSS modules.
-}
type alias Style =
    { layout : CssClass
    , label : CssClass
    , input : CssClass
    , app : CssClass
    }
```

実際に `Style` 型にデコードするために `decoder` 関数を定義しましょう。

```Main.elm
decoder : Decoder Style
decoder =
    Decode.map4 Style
        (Decode.field "layout" CssClass.decode)
        (Decode.field "label" CssClass.decode)
        (Decode.field "input" CssClass.decode)
        (Decode.field "app" CssClass.decode)
```

重要なのは `CssClass.decode` で、これが各 JSON オブジェクトを `CssClass` 型としてデコードします。

```elm
CssClass.decode : Decoder CssClass
```

`CssClass` 型の実態は `Dict String String` ですが、Opaque type として提供しているため、今後ライブラリ側の都合で `Dict String String` 以外に変更したくなっても互換性を壊さないようになっています。

`init` 関数内で実際にデコードする際には以下のようにしています。

```elm
init : Value -> ( Model, Cmd Msg )
init v =
    ( { style =
            Decode.decodeValue decoder v
                |> Result.toMaybe
                |> Maybe.withDefault
                    { layout = CssClass.empty
                    , label = CssClass.empty
                    , input = CssClass.empty
                    , app = CssClass.empty
                    }
      , ...
      , ...
      }
    , ...
    )
```

デコードに失敗したときの初期値として使っている `CssClass.empty` の実態は `Dict.empty` です。

#### view 関数の中で付与したいクラス名を指定する

ここまでで準備が整ったので、実際に DOM にクラスをつけてみましょう。


```elm
import CssClass exposing (CssClass, class)

label : Style -> String -> Html Msg
label style str =
    wrap style
        [ Html.lazy2 label_ style str
        ]


label_ : Style -> String -> Html Msg
label_ style str =
    Html.div
        [ class style.label "default"
        ]
        [ Html.text str
        ]
```

このように、通常使う `Html.Attributes.class` の代わりに `CssClass.class` を使うだけです。

```elm
CssClass.class : CssClass -> String -> Attribute msg
```


### サンプルプロジェクト `examples/real-world`

さて、勘のいい方はお気づきになったことでしょう。
すべての View 関数に引数として `Style` を渡さないと `CssClass.class` が使えません。
そのためには `Style` をバケツリレーのように引き回し続けないといけません。

```elm
inputRow : Style -> String -> (String -> Msg) -> Maybe String -> Html Msg
inputRow style name onInput val =
    wrap style
        [ Html.div
            [ class style.app "inputRow"
            , class style.layout "row"
            , class style.layout "alignCenter"
            ]
            [ label style name
            , expanded style
                [ input style onInput val
                ]
            ]
        ]
```

何回 `style` って書かせるねん！
そこまで規模が大きくなければこれでもいいのですが、規模が大きなプロジェクトで View を細分化するとさすがに堪えられなくなります...

そんなあなたに朗報です！
なんと [arowM/elm-html-with-context](https://github.com/arowM/elm-html-with-context) を使えばこのようなコンテクスト(ここでは `Style`)を明示的に関数に渡さなくても下記のようにすっきり書けます。

```elm
view : Model -> Html Msg
view model =
    WithContext.toHtml (toContext model) <|
        wrap
            [ inputRow "item 1" OnInputInputBox1 model.inputBox1
            , inputRow "item 2" OnInputInputBox2 model.inputBox2
            ]


inputRow : String -> (String -> Msg) -> Maybe String -> Html_ Msg
inputRow name onInput val =
    wrap
        [ WithContext.node
            (\{ style } ->
                -- 明示的に `Style` を渡してないのに、なんと `Style` を関数内で使える！！
                -- めっちゃマジカル！！
                Html.div
                    [ class style.app "inputRow"
                    , class style.layout "row"
                    , class style.layout "alignCenter"
                    ]
            )
            [ label name
            , expanded
                [ input onInput val
                ]
            ]
        ]
```

なんと、CSS modules を使えるライブラリを作っただけでなく、こんなライブラリまで用意してあるなんて、なんと気が利くのでしょう！
しかも、`elm-html-with-context` は CSS modules を使いたい時以外にも、たとえば多言語対応で選択中の言語を各Viewに渡したいときなどにも大活躍します。

`elm-html-with-context` を使った全コードは、[examples/real-world](https://github.com/arowM/elm-css-modules-helper/tree/master/examples/real-world)で見ることができます。
Elm におけるアトミックデザインの簡易な利用例にもなっているので、興味のある方はぜひチェックしてみてください。

## 追記

もっと気軽に CSS modules を Elm で使う方法を記事にしました。
[Elmでもっと気軽にCSS modules](https://qiita.com/arowM/items/ba08d0a705eef2626b69)
実用上はこっちの気軽なやり方で問題なさそうです。

またもや Elm の発展に貢献してしまったので、さくらちゃんへのおやつもお待ちしております！

[さくらちゃんにご飯をあげる](https://www.amazon.co.jp/gp/registry/wishlist/20ZV2VC57615B/ref=nav_wishlist_lists_1)
[![sakura-chan-eating-corn.jpg](https://qiita-image-store.s3.amazonaws.com/0/16345/aaadf636-5b14-0ca0-4a8d-3758a475275a.jpeg)](https://twitter.com/hashtag/%E3%81%95%E3%81%8F%E3%82%89%E3%81%A1%E3%82%83%E3%82%93%E6%97%A5%E8%A8%98?src=hash)