It’s known that Browser.application
and Browser.document
can encounter runtime errors in the wild, due to their assumption that Elm has complete control over the DOM. This assumption doesn’t play nicely with browser extensions.
This recent post by @Ethan_Martin, providing a Webpack-based fix for both Browser.application
and Browser.document
(thank you!), prompted me to share something I discovered recently: a way to create a drop-in replacement for Browser.document
within Elm, without requiring the use of ports to set the document title.
The key to making this work has to do with the lenient way browsers look for the document title. In all the browsers I tested (Firefox, Safari, IE, Chromium-based browsers), they use the first <title>
element they encounter anywhere in the DOM – even if it’s in the <body>
!
This means we can use Browser.element
with Html.node
to provide the <title>
element from Elm. The only restriction is that Elm’s <title>
must be the first <title>
element in the DOM for it to take effect – any earlier <title>
element specified in the HTML, even an empty one, will override it.
Below is a demo app showing the simplest way I found to implement this in Elm. (Note that it won’t work via elm reactor
, which specifies its own <title>
element.)
module Demo exposing (main)
import Browser
import Html
documentElement :
{ init : flags -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
, update : msg -> model -> ( model, Cmd msg )
, view : model -> Browser.Document msg
}
-> Program flags model msg
documentElement { init, subscriptions, update, view } =
Browser.element
{ init = init
, subscriptions = subscriptions
, update = update
, view =
\model ->
let
{ title, body } =
view model
in
Html.div [] <| Html.node "title" [] [ Html.text title ] :: body
}
main : Program () () msg
main =
documentElement
{ init = \_ -> ( (), Cmd.none )
, subscriptions = \_ -> Sub.none
, update = \_ _ -> ( (), Cmd.none )
, view =
\_ ->
{ title = "Hello World!"
, body = [ Html.text "It works!" ]
}
}
Hope this is helpful!