Parse Initial URL without Handling Changes

I’d like to have access to the URL in my init function but I don’t need/want to handle URL changes. Is there a way to do this?

The only way I could find to get access to the url was to use Browser.application but I’m not sure how to set onUrlRequest and onUrlChange to a default handler so that url changes would have the default behavior. Is there a way to get the default behavior for these functions? Or, is there another way to get access to the URL in my init function without using Browser.application? Thanks for any help.

You can use a flag from js on init? And you dont need to handle the url… url = url in nthe model…

Flag is what I do

var app = Elm.FollowingVideos.init({flags: window.location.href})

init : String -> (Model, Cmd Msg)
init href =

Complete Code this was taken from

Using flags would work when I’m using custom HTML, however, it means “elm reactor” won’t work if I’m just viewing my elm app with the default html generated by elm reactor. Is there a way I can have both? Can I get the initial URL without relying on custom HTML and without breaking the default URL functionality?

I haven’t tested this but maybe use the type Json.Value for your flags and decode it in your init. If the decoder fails, because you don’t have custom HTML to provide the URL, then fall back to localhost as the default.

If your application is 100% Elm (you don’t mind Elm taking the body), then the example from Elm guide Navigation example should give you a “default behavior” for URL handling.

You can even remove storing the url in the model, something like:

module Main exposing (Model, Msg(..), init, main, subscriptions, update, view)

import Browser
import Browser.Navigation as Nav
import Url


main : Program () Model Msg
main =
    Browser.application
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        , onUrlChange = UrlChanged
        , onUrlRequest = LinkClicked
        }


type alias Model =
    { key : Nav.Key
    }


type Msg
    = LinkClicked Browser.UrlRequest
    | UrlChanged Url.Url


init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
    ( Model key, Cmd.none )


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, Cmd.none )


view : Model -> Browser.Document Msg
view model =
    { title = "URL Test"
    , body = []
    }


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

May I ask what is your use case though?
Why do you need the URL on init if you don’t need to handle it later?

I’ve got an elm page that renders differently depending on the URL query parameters, i.e.

something.com/foo?param1=bar&param2=baz

Ok, and do you have links in the page?

I’m just learning Elm and it’s first my project so feel free to comment/give any advice. I use python for my webserver and it serves up elm pages and I’m going to implement some services to be able to access the filesystem from elm. The server will be connect to by multiple clients, and they should be able to share URLs to be able to access a specific game and/or scene, which is why I need access to the URL to be able to see which game/scene is being requested.

You display a list a game scenes links to be able to select one:

        SelectGame games ->
            Document (model.game ++ " - scene select")
                [ h1 [] [ text "Select a Scene" ]
                , div [] (Array.toList (Array.map (stringToDiv model.game) games))
                ]


stringToDiv : String -> String -> Html Msg
stringToDiv game scene =
    div []
        [ h1 [] [ Html.a [ href ("Scene.elm?game=" ++ game ++ "&scene=" ++ scene) ] [ text scene ] ] ]

So you could let the links reload the whole page with the new url, but it would be better to just handle it in your app as you already have everything to do it.

For example, move the init logic in a loadGame function, and also call it on UrlChanged.
This will allow to load games smoothly without reloading the page.

Something like:

module Game exposing (main)

import Array
import Browser exposing (Document, UrlRequest)
import Browser.Navigation
import Html exposing (Html, div, h1, text)
import Html.Attributes exposing (href)
import Http
import Json.Decode
import Url exposing (Url)
import Url.Parser exposing ((<?>), query)
import Url.Parser.Query


main : Program String Model Msg
main =
    Browser.application
        { init = init
        , onUrlChange = UrlChanged
        , onUrlRequest = UrlRequested
        , view = view
        , update = update
        , subscriptions = subscriptions
        }


type Msg
    = UrlRequested Browser.UrlRequest
    | UrlChanged Url
    | GotGames (Result Http.Error (Array.Array String))



--------------------------------------------------------------------------------
-- MODEL
--------------------------------------------------------------------------------


type alias Model =
    { game : String
    , page : Page
    , key : Browser.Navigation.Key
    }


type Page
    = NoGame String
    | LoadingScenesJson
    | SceneRequestFailed String
    | SelectGame (Array.Array String)



--------------------------------------------------------------------------------
-- INIT
--------------------------------------------------------------------------------


scenesJsonUrl : String -> String
scenesJsonUrl game =
    "game/" ++ game ++ "/scenes.json"


init : flags -> Url -> Browser.Navigation.Key -> ( Model, Cmd Msg )
init flags url key =
    loadGame url key


loadGame : Url -> Browser.Navigation.Key -> ( Model, Cmd Msg )
loadGame url key =
    case Url.Parser.parse gameUrlParser url of
        Nothing ->
            ( Model "???" (NoGame "url parser failed?") key, Cmd.none )

        Just Nothing ->
            ( Model "???" (NoGame "missing 'game' query parameter") key, Cmd.none )

        Just (Just game) ->
            let
                gameUrl =
                    scenesJsonUrl game
            in
            ( Model game LoadingScenesJson key
            , Http.get
                { url = gameUrl
                , expect = Http.expectJson GotGames scenesDecoder
                }
            )


gameUrlParser : Url.Parser.Parser (Maybe String -> Maybe String) (Maybe String)
gameUrlParser =
    Url.Parser.s "Game.elm" <?> Url.Parser.Query.string "game"


scenesDecoder : Json.Decode.Decoder (Array.Array String)
scenesDecoder =
    Json.Decode.field "scenes" (Json.Decode.array Json.Decode.string)



--------------------------------------------------------------------------------
-- UPDATE
--------------------------------------------------------------------------------


httpErrorMsg : Http.Error -> String
httpErrorMsg error =
    case error of
        Http.BadUrl url ->
            "invalid url: " ++ url

        Http.Timeout ->
            "timeout"

        Http.NetworkError ->
            "network error"

        Http.BadStatus status ->
            "HTTP error status: " ++ String.fromInt status

        Http.BadBody msg ->
            "HTTP sent invalid content: " ++ msg


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        UrlChanged url ->
            loadGame url model.key

        UrlRequested urlRequest ->
            case urlRequest of
                Browser.Internal url ->
                    ( model, Browser.Navigation.pushUrl model.key (Url.toString url) )

                Browser.External href ->
                    ( model, Browser.Navigation.load href )

        GotGames gamesResult ->
            case gamesResult of
                Ok games ->
                    ( { model | page = SelectGame games }, Cmd.none )

                Err error ->
                    ( { model | page = SceneRequestFailed (httpErrorMsg error) }, Cmd.none )



--------------------------------------------------------------------------------
-- SUBSCRIPTIONS
--------------------------------------------------------------------------------


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



--------------------------------------------------------------------------------
-- VIEW
--------------------------------------------------------------------------------


view : Model -> Document Msg
view model =
    case model.page of
        NoGame msg ->
            Document "Error - No Game Selected"
                [ div [] [ text msg ]
                ]

        LoadingScenesJson ->
            Document "Loading..."
                [ div [] [ text ("Loading " ++ scenesJsonUrl model.game) ]
                ]

        SceneRequestFailed error ->
            Document "Failed"
                [ div [] [ text ("failed to load " ++ scenesJsonUrl model.game ++ ": " ++ error) ]
                ]

        SelectGame games ->
            Document (model.game ++ " - scene select")
                [ h1 [] [ text "Select a Scene" ]
                , div [] (Array.toList (Array.map (stringToDiv model.game) games))
                ]


stringToDiv : String -> String -> Html Msg
stringToDiv game scene =
    div []
        [ h1 [] [ Html.a [ href ("Scene.elm?game=" ++ game ++ "&scene=" ++ scene) ] [ text scene ] ] ]
1 Like

Thanks for the tips and taking the time to rework the page to demonstrate them. I’m surprised you took the time to do that, it’s appreciated.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.