sending signal from subcomponent in elm

I’m making a small application in Elm. It displays a timer on the screen, and when the timer reaches zero, it plays a sound. I’m having trouble figuring out how to send a message(?) from the the timer to the sound-player.

Architecturally, I have three modules: a Clock module that represents the timer, a PlayAudio module that can play audio, and a Main module that ties together the Clock module and PlayAudio module.

  • MVC or Rest or Both
  • Single Page Application: advantages and disadvantages
  • how do you folks handle complex state situations where order of operations is important?
  • Ember.js Router App Architecture — How to have multiple nested view/controller pairs
  • JavaScript - How to create a dialog window without using alert() or window.open()?
  • From AngularJS to Flux - The React Way
  • Ideally, when the clock reaches zero, I want to do something like sending a signal from the Clock module. When the clock reaches zero, Clock will send a signal to Main, which will forward it to PlayAudio.

    However, from reading the Elm documentation, it seems like having anything other than Main deal with signals is discouraged. So that leads me to my first question. What is a good way of modeling this change in state? Should the update function from Clock return whether or not it has ended? (This is how I am doing it below, but I would be very open to suggestions about how to do it better.)

    My second question is about how to get the sound to play. I will be using raw Javascript to play the sound, which, I believe, means that I have to use ports. However, I’m not sure how to interact with a port defined in Main from my submodule, PlayAudio.

    Below is the code I am using.

    Clock.elm:

    module Clock (Model, init, Action, signal, update, view) where
    
    import Html (..)
    import Html.Attributes (..)
    import Html.Events (..)
    import LocalChannel (..)
    import Signal
    import Time (..)
    
    -- MODEL
    
    type ClockState = Running | Ended
    
    type alias Model =
        { time: Time
        , state: ClockState
        }
    
    init : Time -> Model
    init initialTime =
        { time = initialTime
        , state = Running
        }
    
    -- UPDATE
    
    type Action = Tick Time
    
    update : Action -> Model -> (Model, Bool)
    update action model =
      case action of
        Tick tickTime ->
            let hasEnded = model.time <= 1
                newModel = { model | time <-
                                        if hasEnded then 0 else model.time - tickTime
                                   , state <-
                                        if hasEnded then Ended else Running }           
            in (newModel, hasEnded)
    
    -- VIEW
    
    view : Model -> Html
    view model =
      div []
        [ (toString model.time ++ toString model.state) |> text ]
    
    signal : Signal Action
    signal = Signal.map (always (1 * second) >> Tick) (every second)
    

    PlaySound.elm:

    module PlaySound (Model, init, update, view) where
    
    import Html (..)
    import Html.Attributes (..)
    import Html.Events (..)
    import LocalChannel (..)
    import Signal
    import Time (..)
    
    -- MODEL
    
    type alias Model =
        { playing: Bool
        }
    
    init : Model
    init =
        { playing = False
        }
    
    -- UPDATE
    
    update : Bool -> Model -> Model
    update shouldPlay model =
        { model | playing <- shouldPlay }
    
    -- VIEW
    
    view : Model -> Html
    view model =
      let node = if model.playing
                    then audio [ src "sounds/bell.wav"
                               , id "audiotag" ]
                               [] 
                    else text "Not Playing"
      in div [] [node]
    

    Main.elm:

    module Main where
    
    import Debug (..)
    import Html (..)
    import Html.Attributes (..)
    import Html.Events (..)
    import Html.Lazy (lazy, lazy2)
    import Json.Decode as Json
    import List
    import LocalChannel as LC
    import Maybe
    import Signal
    import String
    import Time (..)
    import Window
    
    import Clock
    import PlaySound
    
    ---- MODEL ----
    
    -- The full application state of our todo app.
    type alias Model =
        { clock    : Clock.Model
        , player : PlaySound.Model
        }
    
    emptyModel : Model
    emptyModel =
        { clock = 10 * second |> Clock.init
        , player = PlaySound.init
        }
    
    ---- UPDATE ----
    
    type Action
        = NoOp
        | ClockAction Clock.Action
    
    -- How we update our Model on a given Action?
    update : Action -> Model -> Model
    update action model =
        case action of
          NoOp -> model
    
          ClockAction clockAction -> 
              let (newClock, hasEnded) = Clock.update clockAction model.clock  
                  newPlaySound = PlaySound.update hasEnded model.player
              in { model | clock <- newClock
                         , player <- newPlaySound }
    
    ---- VIEW ----
    
    view : Model -> Html
    view model =
        let context = Clock.Context (LC.create ClockAction actionChannel)
        in div [ ]
          [ Clock.view context model.clock
          , PlaySound.view model.player
          ]
    
    ---- INPUTS ----
    
    -- wire the entire application together
    main : Signal Html
    main = Signal.map view model
    
    -- manage the model of our application over time
    model : Signal Model
    model = Signal.foldp update initialModel allSignals
    
    allSignals : Signal Action
    allSignals = Signal.mergeMany
                    [ Signal.map ClockAction Clock.signal
                    , Signal.subscribe actionChannel
                    ]
    
    initialModel : Model
    initialModel = emptyModel
    
    -- updates from user input
    actionChannel : Signal.Channel Action
    actionChannel = Signal.channel NoOp
    
    port playSound : Signal ()
    port playSound = ???
    

    index.html:

    <!DOCTYPE html>
    <html>
     <head>
      <meta charset="UTF-8">
      <script src="js/elm.js" type="text/javascript"></script>
      <link rel="stylesheet" href="style.css">
     </head>
     <body>
            <script type="text/javascript">
                     var todomvc = Elm.fullscreen(Elm.Main);
                    todomvc.ports.playSound.subscribe(function() {
                                    setTimeout(function() {
                                            document.getElementById('audiotag').play();
                                    }, 50);
                    });
            </script>
     </body>
    </html>
    

  • AngularJS: Understanding design pattern
  • From AngularJS to Flux - The React Way
  • What is a good book or resource for writing large ajax applications?
  • What is the most accepted architecture for building a page?
  • How much “single paged” a complex application can be?
  • Single Page Application: advantages and disadvantages
  • One Solution collect form web for “sending signal from subcomponent in elm”

    This approach looks very principled and in accordance with the guidelines of the Elm Architecture post. In that document at the end is a section called One last pattern, which does exactly what you do: have the update function give back a pair if you need to signal from your component to another component.
    So I think you’re doing it right. Of course following this architecture so strictly in such a small application does increase boilerplate/relevant code ratio.

    Anyway, the only changes you need to make are in Main.elm. You don’t actually need a Channel to send a message from a subcomponent to Main, because Main starts the components and wires the update functions together. So you can just use the extra output of the update function of the component and split that off the model signal, into the port.

    ---- UPDATE ----
    
    -- How we update our Model on a given Action?
    update : Clock.Action -> Model -> (Model, Bool)
    update clockAction model =
        let (newClock, hasEnded) = Clock.update clockAction model.clock  
            newPlaySound = PlaySound.update hasEnded model.player
        in ( { model | clock <- newClock
                   , player <- newPlaySound }, hasEnded)
    
    ---- VIEW ----
    
    view : Model -> Html
    view model =
        div [ ]
          [ Clock.view model.clock
          , PlaySound.view model.player
          ]
    
    ---- INPUTS ----
    
    -- wire the entire application together
    main : Signal Html
    main = Signal.map (view << fst) model
    
    -- manage the model of our application over time
    model : Signal Model
    model = Signal.foldp update initialModel Clock.signal
    
    initialModel : Model
    initialModel = emptyModel
    
    port playSound : Signal ()
    port playSound =
      model
      |> Signal.map snd
      |> Signal.keepIf ((==) True)
      |> Signal.map (always ())
    

    Final note: Elm 0.15 is out, and will at the very least simplify your imports. But more importantly interop with JavaScript from within Elm (without ports) is made easier, so as soon as someone creates bindings to a sound library, you should be able to do away with that port.