KarelG
KarelG

Reputation: 5244

Combine multiple signals for one model (using Html.Events and Keyboard signals)

I am fairly new in Elm. I am currently discovering that language while experimenting it. Although the use of signals appears complex. Here below is a working example of my webpage, which handles multiple paragraphs (addition, removal, edition, ...) .

-- start
main: Signal Html.Html
main = Signal.map (view actions.address) model

view: (Signal.Address Action) -> Model -> Html
view address model = ... (displaying paragraphs with buttons, ect)

This snippet initiates the model. By mapping the actions to the model using Signal.map, i am able to handle click events from buttons (imported Html + Event module)

model: Signal Model
model = Signal.foldp update makeEmptyModel actions.signal

Here, i start with an empty model. The update function allows me to update the model after click events on buttons.

update: Action -> Model -> Model
update action model = ....

The Action is a type which handles multiple actions that i have defined like "AddParagraph", "RemoveParagraph" ...

This works so far. Now, when checking the packages, i found Keyboard. That appears interesting, so I want to add a new functionality. If the user presses Alt+A, all paragraphs got selected. (Like Ctrl+A in file explorer)

But it appears that it's not easy to combine that with the current signal mapping that I have. I decided to dig into Signal and found Signal.merge. So i can use it right ? My attempt to merge the keyboard signal to my current signals is

main = Signal.merge
    (Signal.map (view actions.address) model)
    (Signal.map (view actions.address) keysDown)

(the keysDown is from import Keyboard exposing (keysDown) import)

But that doesn't work. I get the next error

The 2nd argument to function `map` is causing a mismatch.
160│      Signal.map (view actions.address) keysDown)
                                            ^^^^^^^^
Function `map` is expecting the 2nd argument to be:
Signal { focused : Bool, selected : Bool, ... }

But it is:
    Signal (Set.Set Char.KeyCode)

It appears that when using Signal.merge, it expects multiple signals which handles the same output. Well that's what i want. But that doesn't work it seems.

Question: How can i add Keyboard signal to the current design ?

Or am I wrong with my expectations about Signal.merge ? That I'm using Signal.merge for wrong purpose ? Should I use Signal.map2 instead ? If so, how can I use it with my current example* ? Or is there a better approach ?

Upvotes: 0

Views: 641

Answers (1)

Chad Gilbert
Chad Gilbert

Reputation: 36375

The Signals you send to merge must be of the same type. You'll need to add another function that maps the keyboard input to an Action prior to merging.

There are a couple ways to achieve this. Here's an example using the basic Counter examples and Signal.merge. Of importance is the use of the keyPressesToAction signal mapping function

import Signal
import Html exposing (..)
import Html.Events exposing (..)
import Keyboard
import Char

type Action = NoOp | Increment | Decrement

actions : Signal.Mailbox Action
actions =
  Signal.mailbox NoOp

update action model =
  case action of
    NoOp -> model
    Increment -> model + 1
    Decrement -> model - 1

model =
  Signal.foldp update 0 (Signal.merge actions.signal keyPressesToAction)

keyPressesToAction =
  let
    keyCodeToAction keyCode =
      case Char.fromCode keyCode of
        '+' -> Increment
        '-' -> Decrement
        _ -> NoOp
  in
    Signal.map keyCodeToAction Keyboard.presses

main =
  Signal.map (view actions.address) model

view address model =
  div []
    [ button [ onClick address Decrement ] [ text "-" ]
    , text <| toString model
    , button [ onClick address Increment ] [ text "+" ]
    ]

Another way to achieve the desired results is by using a port dedicated to converting key presses to action signals. This gets rid of the need to merge signals because it instead causes key presses to trigger the Mailbox you've already set up.

In this scenario, the model function goes back to the way it was, then we add the port below called triggerActionOnKeyPress, which uses the identical keyPressesToAction signal mapping function from above. Here are the relevant lines:

model =
  Signal.foldp update 0 actions.signal

port triggerActionOnKeyPress : Signal (Task.Task Effects.Never ())
port triggerActionOnKeyPress =
  Signal.map (Signal.send actions.address) keyPressesToAction

Upvotes: 3

Related Questions