Elmで複数のページがあるSPA的なのを作りたいと思って公式のチュートリアルを読んでみた。
https://guide.elm-lang.org/webapps/url_parsing.html
パーサーの作り方は載っているのだが、肝心の使い方については、「TODO」の悲しい4文字。。。
色々やってみて、ページを遷移させることについてはなんとか成功したので備忘録として残しておく。
元となるファイル
https://guide.elm-lang.org/webapps/navigation.html
上記のページに載っているソースコードを元に改造する。
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Url
-- MAIN
main : Program () Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
-- MODEL
type alias Model =
{ key : Nav.Key
, url : Url.Url
}
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url, Cmd.none )
-- UPDATE
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Nav.load href )
UrlChanged url ->
( { model | url = url }
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
-- VIEW
view : Model -> Browser.Document Msg
view model =
{ title = "URL Interceptor"
, body =
[ text "The current URL is: "
, b [] [ text (Url.toString model.url) ]
, ul []
[ viewLink "/home"
, viewLink "/profile"
, viewLink "/reviews/the-century-of-the-self"
, viewLink "/reviews/public-opinion"
, viewLink "/reviews/shah-of-shahs"
]
]
}
viewLink : String -> Html msg
viewLink path =
li [] [ a [ href path ] [ text path ] ]
このコードでも、なんとなくブラウザのURLバーのURLは変わってるしHTMLのテキストも変わってるのはわかる。でもURLによって表示するコンテンツをまるごと切り替える、みたいなのはできない。
そこで、さっきもはったURLパースのやり方を書いてるページを参考にコードに書き足していく。
https://guide.elm-lang.org/webapps/url_parsing.html
URLを見る限り、ホーム、プロフィール、レビューの三種類があるようだ。
-- import 文を追加する
import Url.Parser exposing (Parser, parse, (</>), map, oneOf, s, string, top)
-- 中略
type Route
= Home
| Profile
| Reviews String
-- 中略
routeParser : Parser (Route -> a) a
routeParser =
oneOf
[ map Home top
, map Profile (s "profile")
, map Reviews (s "reviews" </> string)
]
公式のチュートリアルに書いてあるのはここまでだけど、実際にURLをパースするには下記のような関数を書く。
参考: https://package.elm-lang.org/packages/elm/url/latest/Url-Parser#parse
toRoute : String -> Route
toRoute string =
case Url.fromString string of
Nothing ->
Home
Just url ->
Maybe.withDefault Home (parse routeParser url)
さらに、現在の状態をmodelで保持できるようにする。
type Page
= HomePage
| ProfilePage
| ReviewsPage String
type alias Model =
{ key : Nav.Key
, url : Url.Url
, page : Page
}
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url HomePage, Cmd.none )
そしてURLが更新されたタイミングでURLをパースするようにupdateを修正。
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
case toRoute (Url.toString url) of
Home ->
( { model | page = HomePage }, Nav.pushUrl model.key (Url.toString url) )
Profile ->
( { model | page = ProfilePage }, Nav.pushUrl model.key (Url.toString url) )
Reviews name ->
( { model | page = ReviewsPage name }, Nav.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Nav.load href )
UrlChanged url ->
case toRoute (Url.toString url) of
Home ->
( { model | url = url, page = HomePage }
, Cmd.none
)
Profile ->
( { model | url = url, page = ProfilePage }
, Cmd.none
)
Reviews name ->
( { model | url = url, page = ReviewsPage name }
, Cmd.none
)
あとはページの種類によって内容を切り替える。
view : Model -> Browser.Document Msg
view model =
{ title = "URL Interceptor"
, body =
[ text "The current URL is: "
, b [] [ text (Url.toString model.url) ]
, case model.page of
HomePage -> ul []
[ viewLink "/home"
, viewLink "/profile"
, viewLink "/reviews/the-century-of-the-self"
, viewLink "/reviews/public-opinion"
, viewLink "/reviews/shah-of-shahs"
]
ProfilePage -> p [] [ text "profile page" ]
ReviewsPage name -> p [] [ text (name ++ "'s review page.") ]
]
}
これで一応URLをパースして表示内容を切り替えることはできるようになった。
多分というか絶対もっといい書き方があると思うが、それはこれからの修行だ。。。
最終的なソースは下記のようになった。
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Url
import Url.Parser exposing (Parser, parse, (</>), map, oneOf, s, string, top)
-- MAIN
main : Program () Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
-- URL Routing
type Route
= Home
| Profile
| Reviews String
routeParser : Parser (Route -> a) a
routeParser =
oneOf
[ map Home top
, map Profile (s "profile")
, map Reviews (s "reviews" </> string)
]
toRoute : String -> Route
toRoute string =
case Url.fromString string of
Nothing ->
Home
Just url ->
Maybe.withDefault Home (parse routeParser url)
-- MODEL
type Page
= HomePage
| ProfilePage
| ReviewsPage String
type alias Model =
{ key : Nav.Key
, url : Url.Url
, page : Page
}
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url HomePage, Cmd.none )
-- UPDATE
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
case toRoute (Url.toString url) of
Home ->
( { model | page = HomePage }, Nav.pushUrl model.key (Url.toString url) )
Profile ->
( { model | page = ProfilePage }, Nav.pushUrl model.key (Url.toString url) )
Reviews name ->
( { model | page = ReviewsPage name }, Nav.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Nav.load href )
UrlChanged url ->
case toRoute (Url.toString url) of
Home ->
( { model | url = url, page = HomePage }
, Cmd.none
)
Profile ->
( { model | url = url, page = ProfilePage }
, Cmd.none
)
Reviews name ->
( { model | url = url, page = ReviewsPage name }
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
-- VIEW
view : Model -> Browser.Document Msg
view model =
{ title = "URL Interceptor"
, body =
[ text "The current URL is: "
, b [] [ text (Url.toString model.url) ]
, case model.page of
HomePage -> ul []
[ viewLink "/home"
, viewLink "/profile"
, viewLink "/reviews/the-century-of-the-self"
, viewLink "/reviews/public-opinion"
, viewLink "/reviews/shah-of-shahs"
]
ProfilePage -> p [] [ text "profile page" ]
ReviewsPage name -> p [] [ text (name ++ "'s review page.") ]
]
}
viewLink : String -> Html msg
viewLink path =
li [] [ a [ href path ] [ text path ] ]
モジュールへの分割などをこれから勉強したい。