Hi.
Elm programs could be used at least in following ways (1) as a fullscreen program or (2) is embed program. It does make sense to start a new web app with a fullscreen program. However, when one wants to introduce Elm to existing code base then embed is the way to go. When DOM element which was used to embed Elm program is removed from the DOM one need to manually kill(unembed, unmount or whatever is called) the Elm program to prevent memory leaks. Memory leaks can be introduced by subscriptions or by HTML event listeners (the view part). At the moment Elm does not have a way to unmount(unembed or kill) the program. One of the workarounds now is to keep a property (e.g. isMounted : Bool
) to track the state of the program. Then one can use isMounted
in the following way.
subscriptions : model -> Sub msg
subscriptions model =
if model.isMounted then
-- Whatever subscriptions are
else
Sub.none -- Empty subscription
view : model -> Html msg
view model =
if model.isMounted then
-- Whatever view is
else
Html.text "" -- Empty view which lets VirtualDom clean all event listeners
This approach adds unnecessary complexity to business logic. But we could automate this work in the following way.
port module Unmountable.Html exposing (Model, Msg, programWithFlags)
import Html
port unmount : (() -> msg) -> Sub msg
type Model internalModel
= Mounted internalModel
| Unmounted
type Msg internalMsg
= Unmount
| InternalModelMsg internalMsg
programWithFlags :
{ init : flags -> ( internalModel, Cmd internalMsg )
, update : internalMsg -> internalModel -> ( internalModel, Cmd internalMsg )
, subscriptions : internalModel -> Sub internalMsg
, view : internalModel -> Html.Html internalMsg
}
-> Program flags (Model internalModel) (Msg internalMsg)
programWithFlags config =
Html.programWithFlags
{ init =
\flags ->
let
( internalModel, internalCmd ) =
config.init flags
in
( Mounted internalModel, Cmd.map InternalModelMsg internalCmd )
, update =
\msg model ->
case model of
Mounted internalModel ->
case msg of
Unmount ->
( Unmounted, Cmd.none )
InternalModelMsg internalMsg ->
let
( newInternalModel, internalCmd ) =
config.update internalMsg internalModel
in
( Mounted newInternalModel, Cmd.map InternalModelMsg internalCmd )
Unmounted ->
( Unmounted, Cmd.none )
, subscriptions =
\model ->
case model of
Mounted internalModel ->
let
unmountSub =
unmount (\_ -> Unmount)
internalModelSub =
config.subscriptions internalModel
in
Sub.batch [ unmountSub, Sub.map InternalModelMsg internalModelSub ]
Unmounted ->
Sub.none
, view =
\model ->
case model of
Mounted internalModel ->
Html.map InternalModelMsg (config.view internalModel)
Unmounted ->
Html.text ""
}
So one could use it as an almost drop-in solution. Instead of Html
one could add import Unmountable.Html as Html
and use Html.programWithFlags
as before. The type signature for main
function will change from Program flags model msg
to Program flags (Html.Model model) (Html.Msg msg)
.
e.g.
module MyProgram exposing (main)
import Unmountable.Html as Html
-- MAIN
main : Program flags (Html.Model model) (Html.Model msg)
main =
Html.programWithFlags
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
-- All the required parts are here
type alias Flags = ...
type alias Model = ...
init : Flags -> ( Model, Cmd Msg )
init flags = ...
type Msg = ...
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model = ...
subscriptions : Model -> Sub Msg
subscriptions model = ...
view : Model -> Html Msg
view model = ...
And on JS side one needs to call the unmount
method of ports
object in the following way.
MyProgram.ports.unmount.send(null);
And that’s it. When there will be the official way to unmount Elm programs one will just remove the dependency and change type signature for main
function back to the original.
My question is: if it worth it to create a package with unmountable programs for most common Elm modules such as Platform
, Html
, Navigation
, elm-css
, etc…?