前回「ElmにクリーンでハックのないCSS modulesを」と称して、Elm で CSS modules を活用する方法について記事にしました。
でも、思ったはずです。「やっぱめんどいな」と。
そうです。僕もです。
ということで、もっと手抜きして簡単に CSS modules の旨味を得る方法をお伝えします。
クラス名の変換表を渡すのがめんどくさい
前回の記事では、flags
を通して、CSS modules から「クラス名をどんな風に変換したか」を示す JSON オブジェクトを渡しました。
その JSON オブジェクトを Elm であつかえる型に変換して各View関数に渡すのですが、ここで引数に渡すバケツリレーが発生してしまいます。
このバケツリレー問題を軽減するために前回は elm-html-with-context
を使いましたが、やっぱりちょっとめんどくさいです。
でもちょっと待ってください。
「どうやって flags
で渡された情報を各View関数に渡すか」を考えるよりも前に
「そもそも flags
で渡された情報を各View関数に渡す必要があるの?」
「そもそも flags
でその情報を受け取る必要があるの?」
「っていうかその情報本当にいるの?」
と考えるべきでしょう。
そのように昨日の僕にお伝えください。
クラス名の変換表を渡さないで解決する
「クラス名をどんな風に変換したか」を示す JSON オブジェクトなしに、どうにかして DOM 側でその変換後のクラス名をつけること方法はないでしょうか?
簡単です。
クラス名の変換規則を決めて、完全に予測できるようにすれば良いんです。
css-loader の localIdentName オプション や postcss-modules の generateScopedName オプション を使えば、クラス名の変換規則を自分で指定することができます。
通常はハッシュ値を使うことが多いですが、ハッシュ値を使わずに「どのCSSのどのクラス名か」を一意に表す命名にすれば良いだけです。
たとえば以下のような設定をすれば、「実際にどのように変換されたか」の対応表がなくても、CSSファイル名と元のクラス名だけ分かれば変換後のクラス名を完全に予想できます。
{
test: /\.css$/,
use: [
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]'
}
}
]
}
これなら Elm 側で DOM にクラス名を付与するときに同様の変換規則に基づいて変換したクラス名を付与するように気をつけるだけで CSS modules を使うことができます。
もちろん、
-
foo.css
の中にbar__baz
というクラス -
foo__bar.css
の中にbaz
というクラス
が存在する場合や
-
src/foo/foo.css
の中にbar
というクラス -
src/foo.css
の中にbar
というクラス
が存在する場合には名前の衝突が起きます。
でも、前者の例であれば「ファイル名に _
を使わない」というルールを設けたり、後者の例であれば localIdentName
に [path]
を入れるなど、運用方法でいくらでもカバーできます。
また、実態としては単にファイル名のプレフィックスをつけるだけですから、「BEM記法を半自動で行うためだけに CSS modules を使っている」とみなせば問題ないことがわかります。
Elm 側の工夫
Elm 側で DOM に変換後のクラス名をつける際、最も簡単なのはそのまま直接 Html.Attributes.class "foo__bar"
のように変換後のクラス名を自分で計算して付与する方法です。
これでも全く問題ないのですが、毎度 foo__
みたいなのをクラス名にくっつけるのが面倒です。
また、何らかの事情で「やっぱり変換規則を [name]__[local]
から [local]-in-[name]
に変更しよう」のような運用フローの変更が起きたときに、直接クラス名を手計算する方法では修正コストが大きくなります。
そこで、1つの工夫の方法として、たとえば以下のような Css.elm
というモジュールを定義してみましょう。
module Css exposing
( Css(..)
, class
, classList
)
import Html exposing (Attribute)
import Html.Attributes as Attributes
{-| 各CSSファイルを表現するカスタムタイプ.
* `Layout` が `layout.css`
* `Input` が `input.css`
* `Label` が `label.css`
* `App` が `app.css`
にそれぞれ対応している
-}
type Css
= Layout
| Input
| Label
| App
{-| 各CSSファイル内にあるクラスにつけるべきプレフィックス
-}
cssPrefix : Css -> String
cssPrefix css =
case css of
Layout ->
"layout__"
Input ->
"input__"
Label ->
"label__"
App ->
"app__"
{-| あるクラスが CSS modules によってどんなクラス名に変換されるか計算する.
たとえば `layout.css` 内に定義されている `row` というクラス名は
className Css.Layout "row"
--> "layout__row"
に変換される
-}
className : Css -> String -> String
className css name =
cssPrefix css ++ name
{-| `Html.Attributes.class` の代わりに使う関数.
-}
class : Css -> String -> Attribute msg
class css name =
Attributes.class <| className css name
{-| `Html.Attributes.classList` の代わりに使う関数.
-}
classList : Css -> List ( String, Bool ) -> Attribute msg
classList css ls =
Attributes.classList <|
List.map (Tuple.mapFirst (className css)) ls
あとはView関数を使う際に
import Css exposing (Css, class)
inputRow : String -> (String -> Msg) -> Maybe String -> Html Msg
inputRow name onInput val =
wrap
[ Html.div
[ class Css.App "inputRow"
, class Css.Layout "row"
, class Css.Layout "alignCenter"
]
[ label name
, expanded
[ input onInput val
]
]
]
のように使うだけです。
こだわりがない人はもっと単純に
{-| `Html.Attributes.class` の代わりに使う関数.
-}
class : String -> String -> Attribute msg
class fileName name =
Attributes.class <| fileName ++ "__" ++ name
みたいな関数を用意するだけでもいいと思います。
実際にこの方法を用いたサンプルを、elm-css-modules-helper の examples/no-flags に用意しておきました。
また、実際にアコーディオンのサンプルもこの方法で実現しています。
以上、もっと気軽に CSS modules を Elm で使う方法でした。
こんな簡単なことをあんなに複雑にやれる elm-css-modules-loader はハッパでもキメてるのかなって思いました。