In the last episode, we built an Etch-A-Sketch of sorts. It's completely usable, but presently kind of annoying because you can't just hold the arrows down on your keyboard to draw.

We'll fix this ourselves by tracking a bit more state and introducing a Time tick. Let's get started.

Project

I've tagged the repo with before_episode_007.3 if you're following along.

First, let's talk about what we want to do fundamentally. I'd like to just tick a value on a subscription 30 times per second, update our model to track the current state of the arrow keys, and then update the model using its internal representation of the arrow keys every time a Tick comes through. First, we'd like to track the arrow key state. Luckily, there's a module called Keyboard.Extra that has this built in for us. We'll check it on any keydown or keyup. First, install the package:

elm-package install ohanhi/keyboard-extra

Then we'll import it and support it in our model:

import Keyboard.Extra


type alias Point = (Int, Int)


type alias Model =
  { points : List Point
  , x : Int
  , y : Int
  , keyboardModel : Keyboard.Extra.Model
  }

Next, we have to handle it in our init and handle a Msg that we'll get from its subscription:

-- We just need to track keyboard state for now
type Msg
  = KeyboardExtraMsg Keyboard.Extra.Msg


-- The Keyboard.Extra is a normal TEA component, so we an get an init out of it
-- and merge it with our own.
init : ( Model, Cmd Msg )
init =
  let
    ( keyboardModel, keyboardCmd ) = Keyboard.Extra.init
  in
    ( { points = [(0, 0)]
      , x = 0
      , y = 0
      , keyboardModel = keyboardModel
      }
    , Cmd.map KeyboardExtraMsg keyboardCmd
    )

We'll add it to our subscriptions so we get notified on keyboard events:


subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.map KeyboardExtraMsg Keyboard.Extra.subscriptions

Honestly, The Elm Architecture (TEA) deserves a week of discussion, and I'll get to that eventually. For now, we're just using it as-is. You can read the Keyboard.Extra README to see how to wire this all up if you needed to yourself.

Next, we completely rewrite our update. We want to do one thing right now - handle the keyboard. We defer to Keyboard.Extra for handling its own messages.

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    KeyboardExtraMsg keyMsg ->
      let
        ( keyboardModel, keyboardCmd ) =
            Keyboard.Extra.update keyMsg model.keyboardModel
      in
        ( { model | keyboardModel = keyboardModel }
        , Cmd.map KeyboardExtraMsg keyboardCmd
        )

OK, so now we're updating our model of the keyboard. We know the state of the arrow keys at all times. Next, we'll introduce a Time tick into our system:

import Time exposing (Time, second)

-- ...
type Msg
  = KeyboardExtraMsg Keyboard.Extra.Msg
  | Tick Time

subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.batch
    [ Sub.map KeyboardExtraMsg Keyboard.Extra.subscriptions
    , Time.every (1/30 * second) Tick
    ]

Now our update function will get a Tick message 30 times a second. Let's handle it:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    -- ...
    Tick _ ->
      let
        {x, y} = Keyboard.Extra.arrows model.keyboardModel
        newX = model.x + x
        newY = model.y + y
      in
        { model | points = (newX, newY) :: model.points, x = newX, y = newY } ! []

Here, we just check the values of the arrow keys and apply them to determine a new point. We can test it out in the browser, and everything works.

However, this is problematic. We've introduced a memory leak of sorts: Even when we're not paying attention to it or updating the path, our update function is adding new items to the list of points. We want to only add new points if either of the arrows is non-zero. We'll use pattern matching to avoid this:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    -- ...
    Tick _ ->
      let
        {x, y} = Keyboard.Extra.arrows model.keyboardModel
        newX = model.x + x
        newY = model.y + y
      in
        case (x, y) of
          (0, 0) ->
            model ! []
          _ ->
            { model | points = (newX, newY) :: model.points, x = newX, y = newY } ! []

With that, everything should be a lot more user friendly. If you want your Etch-A-Sketch to go faster, just increase the argument to Time.every! :)

Summary

In today's episode we saw how to use Time.every and we introduced Keyboard.Extra to make a more advanced version of our Etch-A-Sketch. I hope it was enlightening! See you soon!

Resources

Josh Adams

I've been building web-based software for businesses for over 18 years. In the last four years I realized that functional programming was in fact amazing, and have been pretty eager since then to help people build software better.

  1. Comments for Repeating Keypresses

You May Also Like