50
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

CSS設計完全ガイドで学んだPRE_CSSをElmで堅牢に実装する

Last updated at Posted at 2020-03-01

皆さんは、CSSが得意ですか? 私は大の苦手で、ずっと避け続けています。しかし、何度も何度も向きあおうとしていました。もし共感していただける方には朗報です。

CSS設計完全ガイド

と言う懇切丁寧で、PRE_CSSと言うCSSモジュール設計(OOCSSやBEMなどの仲間です)の設計者本人が書かれた書籍になります。まだ読み初めではありますが、とても感銘を受けた書籍のためレビューの代わりに、Elmでとにかく実践してみると言う形で恩返しをさせていただきます。興味が湧かれた方は是非、購入を検討してみてください。

今回の記事は、肩慣らしということでエレメントモジュール(3.2参照)のボタンに焦点を当ててみました。コードは、こちら

Image from Gyazo

まずは、愚直に実装してみる

今回対象となるボタンは「標準ボタン」「標準ボタンのDisabled状態」「前にアイコンがあるボタン」の三つになります。今回はViewだけに焦点を当てるため、Modelはどのボタンが押されたかの状態を表すもの。Msgもそれに合わせたものになります。

module Main exposing (main)

import Browser
import Html exposing (Html, a, article, div, p, text)
import Html.Attributes exposing (class, href)
import Html.Events exposing (onClick)



-- MAIN


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }



-- MODEL


type ClickState
    = Standard
    | Downloading
    | UnClick


type alias Model =
    ClickState


init : () -> ( Model, Cmd Msg )
init _ =
    ( UnClick, Cmd.none )



-- UPDATE


type Msg
    = StandardClick
    | DownloadClick


update : Msg -> Model -> ( Model, Cmd Msg )
update msg _ =
    case msg of
        StandardClick ->
            ( Standard, Cmd.none )

        DownloadClick ->
            ( Downloading, Cmd.none )



-- VIEW


view : Model -> Html Msg
view model =
    article [ class "ly_cont" ]
        [ div [ class "bl_buttonSimulation" ]
            [ a [ class "el_btn", onClick StandardClick, href "#" ] [ text "標準ボタン" ]
            , a [ class "el_btn el_btn__disabled", href "#" ] [ text "標準ボタン" ]
            , a [ class "el_beforeIconBtn el_beforeIconBtn__download", onClick DownloadClick, href "#" ] [ text "ダウンロード" ]
            , p [ class "bl_buttonSimulation_clickState" ]
                [ text <|
                    case model of
                        Standard ->
                            "標準ボタンがクリックされました"

                        Downloading ->
                            "ダウンロードボタンがクリックされました"

                        UnClick ->
                            ""
                ]
            ]
        ]


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.none

エレメントモジュールのように、どのページ・レイアウトでも使われる可能性があるため、上記のコードのように剥き出しのHTMLタグのままコピー&ペーストを繰り返して開発・運用してしまうと変更に弱く・抽象度が低いためコードの見通しも悪くなってしまいます。

エレメントモジュールを疎結合にする

ElmはSPA時のページ単位ぐらいの粒度以外でのコンポーネント指向は現在推奨されていません(推奨されている時代もありましたが)。しかし、常にコードを具体的に書くという意味ではなく、むしろエレメントモジュールのようなHtml msgについては積極的にモジュール分割や関数による抽象化を推し進めるべきでしょう。今回は、アイコンとボタンにフォーカスを当てて分割と抽象化を行いました。

一つのアプリケーションにおけるアイコンの使用量は基本的に現実的な数で納まり、使用箇所や変更が簡単に行えると実に堅牢です。愚直に書くケースではclass文字列でコードが散りばめられる可能性がありましたが、カスタムタイプとモディファイア(3.3参照)への変換関数を用いて使用することで安全に使用することができます。

module Element.Icon exposing (Icon(..), toButtonModifier)


type Icon
    = Download


toButtonModifier : Icon -> String
toButtonModifier icon =
    case icon of
        Download ->
            "download"

ボタン用のモジュールを作り、画面ごとのオレオレボタンが生成されるのを防ぎましょう。Disabled状態のボタンはMsgを発行される必要がないためプロパティとしてdisabledフラグを受け取ることで、onClickを持つボタンとそうでないボタンに分岐してカプセル化を行いました。beforeIconButtonは先ほど定義したIconを受け取り中で変換することで、確実に存在するアイコンだけを使うことができるようになりました。とても堅牢です。

module Element.Button exposing (beforeIconButton, standardButton)

import Element.Icon as Icon
import Html exposing (Html, a, text)
import Html.Attributes exposing (class, href)
import Html.Events exposing (onClick)


type alias ButtonProps msg =
    { text : String
    , onClick : msg
    , disabled : Bool
    }


standardButton : ButtonProps msg -> Html msg
standardButton props =
    if props.disabled then
        a [ class "el_btn el_btn__disabled", href "#" ] [ text props.text ]

    else
        a [ class "el_btn", onClick props.onClick, href "#" ] [ text props.text ]


type alias IconButtonProps msg =
    { text : String
    , onClick : msg
    , icon : Icon.Icon
    }


beforeIconButton : IconButtonProps msg -> Html msg
beforeIconButton props =
    let
        iconModifier =
            "el_beforeIconBtn__" ++ Icon.toButtonModifier props.icon
    in
    a [ class <| "el_beforeIconBtn " ++ iconModifier, onClick props.onClick, href "#" ] [ text props.text ]

最後は、定義したボタンの関数を呼び出して使ってみましょう。このようにコンポーネント指向でなくても、見通しが高く・再利用性と拡張性に優れたモジュールを作ることができました。ここでのポイントはMsgやボタンの状態であるdisabledなどを閉じ込めずに、プロパティとして渡すことができ、それらは使用側でハンドリングすることです。

view : Model -> Html Msg
view model =
    article [ class "ly_cont" ]
        [ div [ class "bl_buttonSimulation" ]
            [ standardButton { onClick = StandardClick, disabled = False, text = "標準ボタン" }
            , standardButton { onClick = StandardClick, disabled = True, text = "標準ボタン" }
            , beforeIconButton { onClick = DownloadClick, icon = Icon.Download, text = "ダウンロード" }
            , p [ class "bl_buttonSimulation_clickState" ]
                [ text <|
                    case model of
                        Standard ->
                            "標準ボタンがクリックされました"

                        Downloading ->
                            "ダウンロードボタンがクリックされました"

                        UnClick ->
                            ""
                ]
            ]
        ]

まとめ

CSSが大の苦手な私ですが、Elmでさらに堅牢な高いコードにし、実践スピードの速さを上げ、最高の書籍を駆使することで克服の一歩を歩めています。ありがとうございます。CSSもElmもどちらもがんばっていきましょう。

50
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?