3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Elm】どんなページも十把一絡げ! ~ elm-pageでお手軽SPA開発~

Last updated at Posted at 2024-11-04

初めまして!趣味でElmでWebサイトを作っている予備校講師1のkudzuないしはkudzunyanと申します2
記事は初投稿となりますがけっこう気合を入れて書いたのでフィードバックをいただけると嬉しいです。

はじめに

  • この記事は拙作パッケージkudzu-forest/elm-pageの紹介のために書かれたものです。
  • 対象読者: ElmでSPAを
    • やっている人
    • やろうとして挫折した人
    • やってみたいと心の片隅で思っている人
  • 対象読者:
    • SPAのModelMsgの型定義を書くのが楽しくて仕方がない人
      • 引き続き楽しいcoding生活を~:slight_smile:
      • 万一書き飽きてきたら再訪してください。
    • TEAって文字列でお茶しか思い浮かばない人

TL;DR3

  • SPAのページ管理がちょっぴり楽になるパッケージを作りました。
    • Browser.elementBrowser.sandboxを置き換えるだけでページ側のモジュールが定義できます。
    • どんなModelMsgが定義されたページでも、利用側では同じ型のデータとして扱えます。
    • Elmで作成済みのマルチページのWebアプリがあればほぼコピペだけでSPA化できます。

Introduction:今までのSPAの書き方はめんどくさい!

というわけで特に工夫をしない場合のSPAの書き方をおさらいしておきましょう。

本当にめんどっちいのでurl周りは省略してBrowser.elementで書ける範囲だけみてゆきます4
Browser.applicationを用いた本格的なSPAを作る例は当該パッケージのGitHubリポジトリをご覧ください。

工夫しないページモジュールの準備(ここは別にめんどくさくない)

まずは各ページのコンテンツを用意しましょう。
ここではコマンドを用いる例を見たいので、皆さんお馴染みのAn Introduction to Elmからランダムにサイコロの目を表示するアプリを借りてくることにします。

src/Pages/RandomDice.elm
module Pages.RandomDice exposing (Model, Msg, init, subscriptions, update, view)
-- Press a button to generate a random number between 1 and 6.
--
-- Read how it works:
--   https://guide.elm-lang.org/effects/random.html
--

import Browser
import Html exposing (..)
import Html.Events exposing (..)
import Random



-- MAIN


main =
  Browser.element
    { init = init
    , update = update
    , subscriptions = subscriptions
    , view = view
    }



-- MODEL


type alias Model =
  { dieFace : Int
  }


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



-- UPDATE


type Msg
  = Roll
  | NewFace Int


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Roll ->
      ( model
      , Random.generate NewFace (Random.int 1 6)
      )

    NewFace newFace ->
      ( Model newFace
      , Cmd.none
      )



-- SUBSCRIPTIONS


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



-- VIEW


view : Model -> Html Msg
view model =
  div []
    [ h1 [] [ text (String.fromInt model.dieFace) ]
    , button [ onClick Roll ] [ text "Roll" ]
    ]

ここで重要なのはこのモジュールがModel,Msg,init,subscriptions,update,viewを公開していることであり、実を言うと、一行目以外は公式ガイドのコードから全く変更していません5Ctrl-cからのCtrl-vです。

サイコロだけだとsubscriptionsが空で寂しいのでデジタル時計のコードも借りてきましょうか。

Pages/DigitalClock.elmのコード(上とやることは一緒)
src/Pages/DigitalClock.elm
module Pages.DigitalClock exposing (Model, Msg, init, subscriptions, update, view)
-- Show the current time in your time zone.
--
-- Read how it works:
--   https://guide.elm-lang.org/effects/time.html
--
-- For an analog clock, check out this SVG example:
--   https://elm-lang.org/examples/clock
--

import Browser
import Html exposing (..)
import Task
import Time



-- MAIN


main =
  Browser.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }



-- MODEL


type alias Model =
  { zone : Time.Zone
  , time : Time.Posix
  }


init : () -> (Model, Cmd Msg)
init _ =
  ( Model Time.utc (Time.millisToPosix 0)
  , Task.perform AdjustTimeZone Time.here
  )



-- UPDATE


type Msg
  = Tick Time.Posix
  | AdjustTimeZone Time.Zone



update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Tick newTime ->
      ( { model | time = newTime }
      , Cmd.none
      )

    AdjustTimeZone newZone ->
      ( { model | zone = newZone }
      , Cmd.none
      )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
  Time.every 1000 Tick



-- VIEW


view : Model -> Html Msg
view model =
  let
    hour   = String.fromInt (Time.toHour   model.zone model.time)
    minute = String.fromInt (Time.toMinute model.zone model.time)
    second = String.fromInt (Time.toSecond model.zone model.time)
  in
  h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]

これでページモジュールの準備は完了です。

メインモジュールの記述(ここが大変めんどくさい・・・)

さて、前節で準備した二つのページモジュールを利用してSPAを組み上げてゆきましょう。
数多の猛者達がコード自動生成等の秘術を編み出されている模様ですが、
根本的には以下のようなコードを走らせていることかと思います6

src/Main.elm
module Main exposing (..)

import Browser
import Html as H exposing (Html)
import Html.Events as HE
import Pages.DigitalClock
import Pages.RandomDice


type Model
    = RandomDiceModel Pages.RandomDice.Model
      --こんな感じでページ数分の分岐を書く。
    | DigitalClockModel Pages.DigitalClock.Model


type Msg
    = GotRandomDiceMsg Pages.RandomDice.Msg
      --こんな感じでページ数分の分岐を(略)
    | GotDigitalClockMsg Pages.DigitalClock.Msg
    | PageChanged ( Model, Cmd Msg )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case ( msg, model ) of
        ( GotRandomDiceMsg innerMsg, RandomDiceModel innerModel ) ->
            Pages.RandomDice.update
                innerMsg
                innerModel
                |> Tuple.mapBoth
                    RandomDiceModel
                    (Cmd.map GotRandomDiceMsg)

        --こんな感じでページ数分の(略)
        ( GotDigitalClockMsg innerMsg, DigitalClockModel innerModel ) ->
            Pages.DigitalClock.update
                innerMsg
                innerModel
                |> Tuple.mapBoth
                    DigitalClockModel
                    (Cmd.map GotDigitalClockMsg)

        ( PageChanged ( newModel, newCmd ), _ ) ->
            ( newModel, newCmd )

        _ ->
            --リンタの設定によっては怒られるけど握りつぶしとく。
            ( model, Cmd.none )


init : () -> ( Model, Cmd Msg )
init _ =
    Pages.RandomDice.init ()
        |> Tuple.mapBoth
            RandomDiceModel
            (Cmd.map GotRandomDiceMsg)


subscriptions : Model -> Sub Msg
subscriptions model =
    case model of
        RandomDiceModel innerModel ->
            Pages.RandomDice.subscriptions innerModel
                |> Sub.map GotRandomDiceMsg

        --こんな感じで(略)
        DigitalClockModel innerModel ->
            Pages.DigitalClock.subscriptions innerModel
                |> Sub.map GotDigitalClockMsg


view : Model -> Html Msg
view model =
    H.div []
        [ case model of
            RandomDiceModel innerModel ->
                Pages.RandomDice.view innerModel
                    |> H.map GotRandomDiceMsg

            --こんな(略)
            DigitalClockModel innerModel ->
                Pages.DigitalClock.view innerModel
                    |> H.map GotDigitalClockMsg
        , H.button
            [ Pages.RandomDice.init ()
                |> Tuple.mapBoth
                    RandomDiceModel
                    (Cmd.map GotRandomDiceMsg)
                |> PageChanged
                |> HE.onClick
            ]
            [ H.text "サイコロを振る" ]
        , H.br [] []
        , H.button
            [ Pages.DigitalClock.init ()
                |> Tuple.mapBoth
                    DigitalClockModel
                    (Cmd.map GotDigitalClockMsg)
                |> PageChanged
                |> HE.onClick
            ]
            [ H.text "時刻を見る" ]
        ]


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , subscriptions = subscriptions
        , update = update
        , view = view
        }
このコードの解説

まずModelにせよMsgにせよ、各ページに対応したバリアントを用意してそれぞれのデータをwrapしています。

updateではMsg型の値が届いたら、まず現在のModelと同じページ由来のものかをcase分岐で照合して、

  • 違っていたら処理を破棄(何もしない)
  • 合っていたらModelMsgの中身を取り出して(unwrapして)該当するページのupdate関数に渡し、返ってきた( そのページのModel, Cmd そのページのMsg)の各成分を再びwrapして型を合わせる。

ということをしています。参考:Tuple.mapBothCmd.map

上記がわかればinitviewsubscriptionsは流れで読めるかと思うので気合で頑張ってください。参考:Sub.map

いやはや、これはめんどっちいですね・・・。この書き方だと,ページ数にほとんど比例する形でコードの行数が増えてゆきます。ちゃんとしたspaではこれにさらにurlの操作が加わってくるわけで、記述量はさらに増えます7

すべてのページを同じ型のデータとして扱うことでこれらのボイラープレート達を一掃することを狙ったのが、拙作パッケージkudzu-forest/elm-pageになります。

kudzu-forest/elm-pageの紹介

さっそく当該パッケージを用いたSPAの書き方を見てゆきましょう。

英語が大丈夫な方はパッケージサイトも合わせて読んで貰うのが良いかもしれません。chatGPTさんの校正を受けながら書いたのでそんなに酷い英文にはなっていない…はず…。

ページモジュールの修正(めんどくさくない)

まず、Pages/RandomDice.elmに以下のような修正を加えましょう。8

Pages/RandomDice.elm
- module Pages.RandomDice exposing (Model, Msg, init, subscriptions, update, view)
+ module Pages.RandomDice exposing (program)

+ import Page

+ program : Page.Program ()
+ program =
+     Page.element { init = init, subscriptions = subscriptions, update = update, view = view}
修正後のPages.RandomDiceモジュール
src/Pages/RandomDice.elm
module Pages.RandomDice exposing (program)
-- Press a button to generate a random number between 1 and 6.
--
-- Read how it works:
--   https://guide.elm-lang.org/effects/random.html
--

import Browser
import Html exposing (..)
import Html.Events exposing (..)
import Page
import Random



-- MAIN


main =
  Browser.element
    { init = init
    , update = update
    , subscriptions = subscriptions
    , view = view
    }

program =
  Page.element
    { init = init
    , update = update
    , subscriptions = subscriptions
    , view = view
    }


-- MODEL


type alias Model =
  { dieFace : Int
  }


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



-- UPDATE


type Msg
  = Roll
  | NewFace Int


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Roll ->
      ( model
      , Random.generate NewFace (Random.int 1 6)
      )

    NewFace newFace ->
      ( Model newFace
      , Cmd.none
      )



-- SUBSCRIPTIONS


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



-- VIEW


view : Model -> Html Msg
view model =
  div []
    [ h1 [] [ text (String.fromInt model.dieFace) ]
    , button [ onClick Roll ] [ text "Roll" ]
    ]

Pages/DigitalClockにも同様の処理を施します。

デジタル時計のコード
src/Pages/DigitalClock.elm
module Pages.DigitalClock exposing (program)
-- Show the current time in your time zone.
--
-- Read how it works:
--   https://guide.elm-lang.org/effects/time.html
--
-- For an analog clock, check out this SVG example:
--   https://elm-lang.org/examples/clock
--

import Browser
import Html exposing (..)
import Page
import Task
import Time



-- MAIN


main =
  Browser.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }

program : Page.Program ()
program =
  Page.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }


-- MODEL


type alias Model =
  { zone : Time.Zone
  , time : Time.Posix
  }


init : () -> (Model, Cmd Msg)
init _ =
  ( Model Time.utc (Time.millisToPosix 0)
  , Task.perform AdjustTimeZone Time.here
  )



-- UPDATE


type Msg
  = Tick Time.Posix
  | AdjustTimeZone Time.Zone



update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Tick newTime ->
      ( { model | time = newTime }
      , Cmd.none
      )

    AdjustTimeZone newZone ->
      ( { model | zone = newZone }
      , Cmd.none
      )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
  Time.every 1000 Tick



-- VIEW


view : Model -> Html Msg
view model =
  let
    hour   = String.fromInt (Time.toHour   model.zone model.time)
    minute = String.fromInt (Time.toMinute model.zone model.time)
    second = String.fromInt (Time.toSecond model.zone model.time)
  in
  h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]

メインモジュールの記述(あんまりめんどくさくない!!)

では、これらの子モジュールを呼び出す親モジュール側の書き方を見てゆきましょう。
親モジュール側でPageにより抽象化された子モジュールのModelMsgをハンドリングするために、
Pageには以下の4つの関数が定義されています。

ハンドリング用の関数:
Page.mapInit :
    ( (Page.Model, Cmd Page.Msg) -> appMsg )
      -> Page.Program flag
      -> flag
      -> appMsg

Page.mapSubscriptions : ( Page.Msg -> appMsg ) -> Page.Model -> Sub appMsg

Page.mapView : ( Page.Msg -> appMsg ) -> Page.Model -> Html appMsg

Page.update : Page.Msg -> Page.Model -> ( Page.Model, Cmd Page.Msg )

ここで,appMsgはページを利用する側のモジュール(今回の記事ではsrc/Main.elm)で定義されたMsgです。
関数名と型注釈から各々の役割を想像してみてください。
イメージとしては、

  • Page.Modelが子モジュールのModelに相当する型。
  • Page.Msgは子モジュールのMsgに相当する型
  • mapInitは、
    • 普通のアプリケーションでinit(Model, Cmd Msg)を返すのと同様に、Page.Program flagflagを渡して初期化したらモデルとコマンドのペア( Page.Model, Cmd Page.Msg)が返ってくる仕組みになっている。
    • ただし、それを親モジュールのメッセージ(ここではappMsgという型名になっている。)に変換する必要があるので、その変換器を第一引数として渡す。
  • mapView及びmapSubscriptionsは、
    • 第一引数としてPage.Msgから親モジュールのappMsgへの変換タグを渡す。
    • 親モジュールのupdateで、Page.Msgを当該Page.ModelとともにPage.updateに渡し、子モジュールのモデルとコマンドのペア( Page.Model, Cmd Page.Msg)による更新処理を記述する。

という感じです。
ここで特筆すべきは、Page.ModelにしろPage.Msgにしろ、型変数を持っていないという点です。
要は、Pages.RandomDiceで定義されたprogramPages.DigitalClockで定義されたprogramも、親モジュールから見ると同じ型の値として扱えます。

これらを用いた親モジュールの書き方は以下のような感じになります。

src/Main.elm
module Main exposing (..)

import Browser
import Html as H exposing (..)
import Html.Events as HE
import Page
import Pages.DigitalClock as DigitalClock
import Pages.RandomDice as RandomDice


type alias Model =
    -- ページごとのバリアントは不要!
    { page : Page.Model
    }


type
    Msg
    -- ページごとバリアントは不要!
    = PageInitialized ( Page.Model, Cmd Page.Msg )
    | GotPageMsg Page.Msg


init : () -> ( Model, Cmd Msg )
init _ =
    -- ここでは親モジュールの初期化時に
    -- 子モジュールの初期化も同時に行っている。
    update
        (Page.mapInit PageInitialized
            RandomDice.program
            ()
         --  この()はflag。
        )
        (Model Page.empty)


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PageInitialized ( initialPage, initialCmd ) ->
            ( { model | page = initialPage }
            , Cmd.map GotPageMsg initialCmd
            )

        GotPageMsg pMsg ->
            let
                ( newPage, pageCmd ) =
                    Page.update pMsg model.page
            in
            ( { model | page = newPage }
            , Cmd.map GotPageMsg pageCmd
            )


subscriptions : Model -> Sub Msg
subscriptions model =
    Page.mapSubscriptions GotPageMsg model.page


view : Model -> Html Msg
view model =
    div []
        [ Page.mapView GotPageMsg model.page
        , H.br [] []
        , H.text ""
        , H.br [] []
        , button
            [ HE.onClick <|
                Page.mapInit PageInitialized
                    RandomDice.program
                    ()
            ]
            [ text "サイコロを振る" ]
        , H.br [] []
        , button
            [ HE.onClick <|
                Page.mapInit PageInitialized
                    DigitalClock.program
                    ()
            ]
            [ text "時刻を見る" ]
        ]


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

各モジュールに関する場合分けが不要となったため、
コード量がいくらか削減されています。
この効果はページ数が多くなるほど絶大です。

SPAのコード量に圧倒されて手が出ていないという方がいらっしゃいましたら
是非当パッケージをお試しください。

また、このパッケージのGitHubリポジトリを見てもらえば、

  • もっと簡単な例
  • もっとページ数の多い例
  • Urlまわりを扱っている例(親モジュールでBrowseer.application使うやつ)

等も御覧いただけます。

このパッケージの仕組み(気になる人向け)

ここから先はよろしければ
GitHubリポジトリsrc/Page.elmと照合しながらご覧いただければと思います。

さて,実装についてですが,まず一言謝罪させてください。このモジュールの関数名や型定義は利用者を若干騙すような設計になっています。使う分にはわかりやすくなっていると思うのですが、あまり名前のイメージに引っ張られると実装が読みにくくなると思うので気を付けてください。

では内容に入ってゆきましょう。

型の内部実装

本パッケージで扱うPage.ModelPage.MsgPage.Program flagの型定義は以下のようになっています。このコードで登場するModelMsgという文字列は全て以下のPageモジュールで定義された値であることに気を付けてください。

Page

type Model
    = Model
        { key : Unique.Unique
        , html : Html Msg
        , sub : Sub Msg
        }

type Msg
    = Updated ( () -> ( Model, Cmd Msg) )

type Program flag
    = Program ( flag -> ( Model, Cmd Msg) )
Unique.Uniqueについて

harrysarson/elm-hacky-uniqueというパッケージ9で定義されたもので、「メッセージを吐いたページと更新しようとしているページが同じページか」という判定を行うために利用しています。ここでいう『同じ』とは、『同じPage.mapInitの__呼び出し__で初期化された』という意味で、mapInitに渡す引数が等しくともタイミングが違えば『異なるページ』と判断されます。

Opaque typeを得るため&型の無限ループを許容するためのタグを無視すれば、Page.ModelHtml Page.MsgSub Page.Msgの組で、
Page.Msgは組( Page.Model, Cmd Page.Msg)を得る処理を遅延させたものであるようです。
この段階ですでに再帰的構造になっていてわかりにくいですが、
これと先ほどの4関数を見比べると処理内容が想像しやすいかもしれません。

各関数の実装

実はPage.updateは型シグネチャこそ我々が普段書いているupdate関数に寄せてありますが、やっていることはPage.Msg型のunwrapにすぎません。その際、第一引数の内部から引っ張り出したPage.Modelと第二引数として渡したPage.Modelのキー(Unique.Unique)を比較し、不一致な場合は処理を放棄する設計になっています。
この設計により時間がかかる処理をコマンドで出してから他のページに遷移した際、処理が終わって前のページに強制的に引き戻されるという事象を防ぐことができます。同時に、バックグラウンドでPage.Modelを保持しておけばそちらのアップデートは可能となっています。

また、mapViewmapSubscriptionsは単純にPage.Modelの中身を取り出して第一引数の関数を用いてHtml.mapSub.mapで変換しているだけです。
mapInitも似たようなものですね。

そうなると、各子モジュールの方で定義されたModel型の値はどこに保存されているのか、という事が気になってきます。

Pages.DigitalClock.Modelのありか

結論から言うと、Page.Modelhtmlsubの中に保存されている、という事になります。

Html msgが状態をもてる、という極端な例として以下の独立したElmプログラムを動かしてみると良いかもしれません。

module MinimalCounter exposing (..)

import Browser
import Html as H exposing (Html)
import Html.Events as HE


type alias Model =
    Html Msg


type Msg
    = Updated (() -> Model)


init : Int -> Html Msg
init n =
    H.div []
        [ H.button
            [ HE.onClick <| Updated (\() -> init (n + 1))
            ]
            [ H.text "+" ]
        , H.text <| String.fromInt n
        ]


main : Program () Model Msg
main =
    Browser.sandbox
        { init = init 0
        , update = \(Updated f) model -> f ()
        , view = identity
        }

このコードではModel自身はIntを保持していないにもかかわらず、
あたかも整数値が記録されているかのように振る舞います。
まったく同じ定義のModelMsgでも、initの部分だけうまくいじれば
String型の値を保持させることもできるでしょう。
要はHtml MsgMsgを生じるために必要な情報としてModelを保持させてしまえばいいのです。
この関数をHtmlのツリー構造にハードコードするのではなく、
Html.mapを用いて外部に切り出すことも可能です。
この発想をコンパイラの協力のもと発展させていけば、
ModelMsgが隠蔽されたsandbox相当の実装まで辿り着けます。

演習:`sandbox`相当の実装(※本パッケージのものとは異なる)

解答例:

module SandboxTest exposing (..)

import Browser
import Html as H exposing (Html)
import Html.Events as HE


type OuterMsg
    = Updated (() -> Html OuterMsg)


type alias Model =
    Html OuterMsg


sandbox :
    { init : model
    , update : msg -> model -> model
    , view : model -> Html msg
    }
    -> Html OuterMsg
sandbox { init, update, view } =
    let
        mapper : model -> msg -> OuterMsg
        mapper model msg =
            Updated <|
                \() ->
                    sandbox
                        { init = update msg model
                        , update = update
                        , view = view
                        }
    in
    H.map (mapper init) <| view init
    
-- 使用例:


type InnerMsg
    = Increment
    | Decrement


type alias InnerModel =
    Int


counter : Html OuterMsg
counter =
    -- 別モジュールに切り分けてもOK!
    let
        init : InnerModel
        init =
            0
            
        update : InnerMsg -> InnerModel -> InnerModel
        update msg model =
            case msg of
                Increment ->
                    model + 1

                Decrement ->
                    model - 1
                    
        view : InnerModel -> Html InnerMsg
        view model =
            H.div []
                [ H.button [ HE.onClick Decrement ] [ H.text "-" ]
                , H.br [] []
                , H.text <| String.fromInt model
                , H.br [] []
                , H.button [ HE.onClick Increment ] [ H.text "+" ]
                ]
    in
    sandbox { init = init, update = update, view = view }
    
type alias AppModel =
    Html OuterMsg
    
type AppMsg
    = GotOuterMsg OuterMsg
    
main : Program () AppModel AppMsg
main =
    let
        init : AppModel
        init =
            counter
            
        update : AppMsg -> AppModel -> AppModel
        update msg model =
            case msg of
                GotOuterMsg (Updated f) ->
                    f ()
                    
        view : AppModel -> Html AppMsg
        view model =
            H.map GotOuterMsg model
    in
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

このコードはこのままEllieで動かせます。
色々弄って遊んでみてください。

残りの作業

ここまででカギとなるアイデアは紹介できました。さらに副作用を扱えるようにするためには、Cmd MsgSub Msgの取り扱いが必要になりますが、これは上記のように型を調製し、Html.mapCmd.map、及びSub.mapを併用すれば良いだけです。
後の細かい部分は実装を眺めてみてください。

実装紹介の終わりに

ここで紹介したアイデア、特にHtmlへの状態の埋め込みなどはTEAライクなモジュール化を狙うあらゆるパッケージ設計において利用可能なものかと思うので10
これからパッケージを作ろうという方はぜひ状態の管理方法として検討に入れて頂ければと思います。

ではでは皆さま、Happy Elm coding!

  1. 数学とか物理とかを教えています。作品例:指数・大数計算練習

  2. どちらを使うかは「にゃん」が許される空気かによります。

  3. TL;DRというのは「too long; did'nt read=長すぎて読めんかったわこのタワケが!」の略だそうですね。すでに青筋が立ち始めていてかつSPAのコードに慣れ親しんでいる方は、拙作パッケージを用いた書き方まで飛んでいただいても大丈夫です。

  4. 要は「複数ページのModelMsgのハンドリングの部分だけ解説する」ということです。ページの切り替えはボタン操作にしちゃいます。

  5. 各ページごとのmain関数は本来不要となりますが、別に書いてあっても邪魔にはならないので放っておいてOKです。そうすればデバッグ時は問題のあるページのみをコンパイルして動作確認をすることができ、より親切なエラーメッセージを享受しながらリファクターできます。

  6. 特にリサーチはしていないし自動生成とかは使ったことがないので,見当違いでしたらすみません。

  7. urlのパース処理は「ボイラープレート」というほど単調ではないかもしれませんが、ここでは割愛します。

  8. 言い換えると、拙作パッケージからPageモジュールをimportし、Browser.elementと同じ引数をPage.elementに渡した返値を公開してください、ということです。

  9. elm-hacky-uniqueはElmがNaN同士の==による比較でFalseを吐くというある種のバグを利用して、Elmの世界にJavaScriptオブジェクトの参照比較の概念を持ち込むという闇の魔術をひっそりと実行しているチャレンジングなパッケージです。明日にでもEvan氏がコンパイラを修正して機能を失うリスクが0ではないです。(パフォーマンスの都合で放っておかれているバグだと思われるので限りなく0に近いです。万一の場合はelm-pageもユーザーに一意なキーを登録してもらうような形になるかと思います。)

  10. そもそもこのパッケージを作った動機がクイズアプリのための別のパッケージを書いている途中で「Htmlへの状態の埋め込み」を思いついたので、それでどこまでゆけるか試してみよう、ということだったりします。思ったより遠くまでこれたので皆様とシェアさせていただきたくパッケージを整えてこの記事を書いた次第です。

3
0
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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?