I'm stuck refactoring code for a page with multiple media elements

I’m new to Elm and have spent some time getting used to it. I’ve used a fork of @Dan_Abrams Elm Media.

The code with the problem is here and a live example here. I simply would like to play/pause audio and display its progress.

There are a two core issues I would like feedback on:

  • How do you structure your Elm code when you need multiple interactive elements on the page? This doesn’t fit the classic Spa example pattern since that page is built around routes with multiple “pages”. Nor does it fit the Elm sortable table example.

  • The other issue is that the code is currently broken, the play button does not work (in fact the media elements rarely load properly, it works better locally). It seems the media element does not feed back it’s state back to the Elm runtime.

Any help would be appreciated.

For the second question, the current source code is strange as it does not seem to load the js part of elm-media.

As a quick test, I copied media/Port/mediaApp.js to ./buid/ then following elm-media instruction, I changed build/index.html to:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Mixer</title>

    <meta name="msapplication-TileColor" content="#da532c" />
    <meta name="theme-color" content="#ffffff" />
    <link
      rel="stylesheet"
      href="https://unpkg.com/purecss@1.0.1/build/pure-min.css"
      integrity="sha384-"
      crossorigin="anonymous"
    />

    <link rel="stylesheet" href="https://unpkg.com/purecss@1.0.1/build/grids-responsive-min.css" />
    <script src="elm.js"></script>
    <script src="mediaApp.js"></script>
  </head>
  <body>
    <div id="elm-app"></div>
    <script>
      MediaApp.Modify.timeRanges();
      MediaApp.Modify.tracks();
      var app = Elm.Main.init({
        node: document.getElementById('elm-app')
      });
      app.ports.outbound.subscribe(MediaApp.portHandler);
    </script>
  </body>
</html>

The only additions are:

<script src="mediaApp.js"></script>

then

MediaApp.Modify.timeRanges();
MediaApp.Modify.tracks();
app.ports.outbound.subscribe(MediaApp.portHandler);

And the medias then play correctly and can be mixed together.

For the first question, could you give more details about your code structuring issue (maybe an example of what code part seems wrong to you)?

As an aside, why do you log every (msg, model) in update as you have enabled the debugger? (just click the elm logo on bottom right, you will have every message with the corresponding model)

Thanks, that solves that pretty much both problems. I thought I had structured my code incorrectly.

My problem is solved by the first question. But one thing I find is that I’m essentially casting from Msg to OtherModel.Msg (same for Model) many times over, as follows:

updatePlayerGroup : PlayerGroupMsg -> PlayerGroupModel -> ( PlayerGroupModel, Cmd PlayerGroupMsg )
updatePlayerGroup msg model =
    case msg of
        Original subMsg ->
            let
                ( updatedAudioPlayerModel, updatedCmd ) =
                    AudioPlayer.update subMsg model.original
            in
            ( { model | original = updatedAudioPlayerModel }
            , Cmd.map Original updatedCmd
            )
...

Are there more efficient and idiomatic ways of doing this?

That’s just a legacy from running Elm without a debugger. I just discovered the --debug flag in elm make this morning.

1 Like

It looks fine to me, but you could make an helper if you wanted.
For example:

        Mix1 subMsg ->
            let
                ( updatedAudioPlayerModel, updatedCmd ) =
                    AudioPlayer.update subMsg model.mix1
            in
            ( { model | mix1 = updatedAudioPlayerModel }
            , Cmd.map Mix1 updatedCmd
            )

could be written

        Mix1 subMsg ->
            AudioPlayer.update subMsg model.mix1
                |> Tuple.mapBoth (\p -> {model | mix1 = p}) (Cmd.map Mix1)        

So you could use that or have the following helper:

updatePlayer :
    (Model -> AudioPlayer.Model)
    -> (AudioPlayer.Msg -> Msg)
    -> (AudioPlayer.Model -> Model)
    -> AudioPlayer.Msg
    -> Model
    -> ( Model, Cmd Msg )
updatePlayer toPlayer toMsg toModel msg model =
    AudioPlayer.update msg (toPlayer model)
        |> Tuple.mapBoth toModel (Cmd.map toMsg)

and write

        Mix1 subMsg ->
            updatePlayer .mix1 Mix1 (\player -> { model | mix1 = player }) subMsg model

Also given the current source code, I would remove the whole PlayerGroup thing that nests a lot of things for no obvious benefit.

So the whole Main.elm could become something like:

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

import AudioPlayer
import Browser
import Html exposing (Html, button, div, h1, p, text)
import Html.Attributes exposing (class)
import Html.Events exposing (onClick)



-- GLOBALS


siteTitle : String
siteTitle =
    "Mix"



-- MODEL


type alias Model =
    { original : AudioPlayer.Model
    , mix1 : AudioPlayer.Model
    , mix2 : AudioPlayer.Model
    }


init : () -> ( Model, Cmd Msg )
init _ =
    ( { original = AudioPlayer.new "sample-original" "sample/original.mp3"
      , mix1 = AudioPlayer.new "sample-mix1" "sample/mix1.mp3"
      , mix2 = AudioPlayer.new "sample-mix2" "sample/mix2.mp3"
      }
    , Cmd.none
    )



-- UPDATE


type Msg
    = Original AudioPlayer.Msg
    | Mix1 AudioPlayer.Msg
    | Mix2 AudioPlayer.Msg


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Original subMsg ->
            updatePlayer .original Original (\player -> { model | original = player }) subMsg model

        Mix1 subMsg ->
            updatePlayer .mix1 Mix1 (\player -> { model | mix1 = player }) subMsg model

        Mix2 subMsg ->
            updatePlayer .mix2 Mix2 (\player -> { model | mix2 = player }) subMsg model


updatePlayer :
    (Model -> AudioPlayer.Model)
    -> (AudioPlayer.Msg -> Msg)
    -> (AudioPlayer.Model -> Model)
    -> AudioPlayer.Msg
    -> Model
    -> ( Model, Cmd Msg )
updatePlayer toPlayer toMsg toModel msg model =
    AudioPlayer.update msg (toPlayer model)
        |> Tuple.mapBoth toModel (Cmd.map toMsg)



-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ viewHeader
        , viewDemo model
        , viewFooter
        ]


viewHeader : Html Msg
viewHeader =
    div [ class "banner" ]
        [ h1 [ class "banner-head" ] [ text siteTitle ]
        ]


viewDemo : Model -> Html Msg
viewDemo { original, mix1, mix2 } =
    div [ class "pure-g" ]
        [ viewPlayer original
            |> Html.map Original
        , viewPlayer mix1
            |> Html.map Mix1
        , viewPlayer mix2
            |> Html.map Mix2
        ]


viewPlayer : AudioPlayer.Model -> Html AudioPlayer.Msg
viewPlayer player =
    div [ class "pure-u-1-3" ]
        [ p []
            [ text player.tag ]
        , div []
            [ AudioPlayer.view player
            ]
        ]


viewFooter : Html Msg
viewFooter =
    div [] []



-- MAIN


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }

So the whole Main.elm could become something like:

Thanks. Phew! that will take a bit to digest.

What does .original syntax mean in?:

 Original subMsg ->
            updatePlayer .original Original (\player -> { model | original = player }) subMsg model
1 Like

model.original is the same as .original model.
So it is a function that returns the original field of a record parameter.

In elm repl:

> .original
<function> : { b | original : a } -> a

See Understanding extensible records.

I have edited my answer with a little more explanations.

1 Like

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