DEV Community

Kristian Pedersen
Kristian Pedersen

Posted on

#30daysofelm Day 20: Getting the browser's width and height

This is day 20 of my 30 day Elm challenge

Today I wanted to do something visual, but I wanted the SVG to fill up the available space.

Instead, this turned out to be a day where we learn about getting information about the browser's width and height.

In JavaScript, I can just ask for innerWidth and innerHeight. In Elm, it's more difficult.

This post is written with confusion and poor sleep. :)

Code/demo: https://ellie-app.com/bZDy65SqXXFa1

Table of contents

1. Getting the browser's width and height

My experience with Elm documentation has been a bit frustrating. The descriptions are often good, and the type annotations help, but there have been a few occasions where just one or two code examples would have helped a lot.

For example, Browser.Dom.getViewport sounds good, but how do I use it? Its type annotation says Task x Viewport. I've seen Task mentioned before, and Viewport is explained very well, but what on earth is an x?

Of course, I should have read the documentation more thoroughly, but just having a practical example would have been nice for developers at any level.

The type alias is easy to understand:

type alias Viewport =
    { scene :
          { width : Float
          , height : Float
          }
    , viewport :
          { x : Float
          , y : Float
          , width : Float
          , height : Float
          }
    }
Enter fullscreen mode Exit fullscreen mode

However, when trying to return Browser.Dom.getViewport.scene in a function, I get this error:

This is not a record, so it has no fields to access!
23| Browser.Dom.getViewport.scene
^^^^^^^^^^^^^^^^^^^^^^^
This getViewport value is a:
Task.Task x Browser.Dom.Viewport
But I need a record with a scene field!

Well excuse me for reading the type alias and thinking curly braces equal a record. ;)

So I read up on the Task documentation, and revisit the time example, and try Task.perform GetBrowserDimensions Browser.Dom.getViewport, with the following code:

type Msg
    = GetBrowserDimensions


dimensions =
    Task.perform GetBrowserDimensions Browser.Dom.getViewport
Enter fullscreen mode Exit fullscreen mode

This resulted in a type of error message I've seen before, but I still struggle a bit with. Msg and msg - not a good choice of convention. :/

28| Task.perform GetBrowserDimensions Browser.Dom.getViewport
^^^^^^^^^^^^^^^^^^^^
This GetBrowserDimensions value is a:
Msg
But perform needs the 1st argument to be:
a -> msg

I asked at the Elm Slack channel, and got help very quickly:

Samuel Kacer 28 minutes ago
The first argument to Task.perform needs to be a function that will take the result from the Task and wrap it in some kind of message. so for the case of getViewPort, the argument needs to be of type Viewport -> Msg.
the first argument you are providing, GetBrowserDimensions, is of type Msg, so I assume it doesn't contain anything and has a definition something like this:
type Msg =
...
| GetBrowserDimensions
but instead needs to be something like
| GetBrowserDimensions Viewport
that way the constructor for that message variant will have a type of Viewport -> Msg, which would fit for the Task you are wanting to perform

arkham 27 minutes ago
you can check out this ellie https://ellie-app.com/bZvHnKqpPrCa1

Also, I decided I needed it to respond to window resize events, which was also confusing, since the onResize documentation uses Cmd Msg as its type, but apparently I needed to use a Sub Msg in my case:

Kristian Pedersen 2 hours ago

2. Responding to window resize events

Actually, I realized I wanted it to update on window resize. Again, I think I’m almost there, but it’s telling me I need a sub msg, not a cmd msg:
https://ellie-app.com/bZBqjmgPS9pa1
What also confuses me is that going by the documentation, the subscriptions function returns a cmd msg, but in my example, it need to be a sub msg: https://package.elm-lang.org/packages/elm/browser/latest/Browser-Events#onResize

arkham 1 hour ago
hey @kristian Pedersen, it’s just the type of the subscription is a Sub Msg instead of a Cmd Msg , so your subscriptions function should be a Sub Msg, here’s a working ellie https://ellie-app.com/bZC6k6dv9wna1

arkham 1 hour ago
I also converted the Ints to Floats to get the type checker to be happy

arkham 1 hour ago
and here’s a very simple example of a subscription: https://guide.elm-lang.org/effects/time.html

arkham 1 hour ago
oh, and to be clear: the documentation is saying that onResize returns a Sub msg https://package.elm-lang.org/packages/elm/browser/latest/Browser-Events#onResize

Thanks for the help and patience, Arkham! You're a legend.

3. The code

Once all the confusion and going back and forth had settled, my resulting code mostly looks pretty nice, to be honest.

3.1. Model and Msg

type alias Model =
    { width : Float, height : Float }


initialModel : Model
initialModel =
    { width = 0, height = 0 }


type Msg
    = NoOp
    | GotInitialViewport Viewport
    | Resize ( Float, Float )
Enter fullscreen mode Exit fullscreen mode

The model is straight forward. Although GotInitialViewport and Resize look different, they both involve dealing with two Floats.

I don't really like how this looks. Maybe it would have been cleaner to just do it through JavaScript interop?

3.2. Main and subscription

main : Program () Model Msg
main =
    let
        handleResult v =
            case v of
                Err err ->
                    NoOp

                Ok vp ->
                    GotInitialViewport vp
    in
    Browser.element
        { init = \_ -> ( initialModel, Task.attempt handleResult Browser.Dom.getViewport )
        , view = view
        , update = update
        , subscriptions = subscriptions
        }


subscriptions : model -> Sub Msg
subscriptions _ =
    E.onResize (\w h -> Resize ( toFloat w, toFloat h ))
Enter fullscreen mode Exit fullscreen mode

That's a pretty chunky main function compared to what I've seen before.

When the task handleResult is done, it will return one of those two Cmd Msgs in the let statement.

3.3. Update

setCurrentDimensions model ( w, h ) =
    { model | width = w, height = h }


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GotInitialViewport vp ->
            ( setCurrentDimensions model ( vp.scene.width, vp.scene.height ), Cmd.none )

        Resize ( w, h ) ->
            ( setCurrentDimensions model ( w, h ), Cmd.none )

        NoOp ->
            ( model, Cmd.none )
Enter fullscreen mode Exit fullscreen mode

Cmd.none just seems like it could be implicit instead, although I guess Elm favors explicitness a lot more than I'm used to.

It adds a bit extra overhead to me as a beginner, but I guess it can be nice to see Cmd.none at a glance.

Again, I don't like my double approach, where I get the width and height two different ways: through a vp variable, and through a ( w, h ) tuple. It just feels wrong.

3.4. View

Just displaying some data. A nice ending to a confusing day:

view : Model -> Html Msg
view model =
    div []
        [ text
            ("The width is "
                ++ (model.width |> String.fromFloat)
                ++ "px, and the height is "
                ++ (model.height |> String.fromFloat)
                ++ "px"
            )
        ]
Enter fullscreen mode Exit fullscreen mode

4. Conclusion

This is one case where I don't immediately see the benefit in doing it the Elm way, rather than just doing it through JavaScript interop, using window.eventListener.

I definitely need to re-read the Browser.Dom and Task documentation.

What I also need is a good night's sleep. (I highly recommend "Why We Sleep" by Matthew Walker)

I woke up way too early, didn't have a siesta, and I'm noticing the negative effects on my thinking and mood. I've been through frustrating learning moments before, so I'll get through this one as well.

Get a good night's sleep you too, and see you tomorrow!

Top comments (2)

Collapse
 
wolfadex profile image
Wolfgang Schuster

When you say

That's a pretty chunky main function compared to what I've seen before.

I completely agree. It would be a lot better to change from GotInitialViewport Viewport to GotInitialViewport (Result x Viewport) (I'm not entirely sure what the type of the error is there). This moves the error handling to your update instead of your init.


I'm still catching up as I'm about 5 days behind, but from today's post it seems like you've been struggling with type variables a lot. Taking a look at guide.elm-lang.org/types/reading_t... another time might help. I don't have any specific resources for learning more about types but I'll try to find some.

Collapse
 
kristianpedersen profile image
Kristian Pedersen

Thanks for the link! Yeah, that's an area I've been confused by, particularly when reading documentation.

I was kind of frustrated by the Task x Viewport, but reading the documentation again now, I see that it refers to an unsuccessful Task execution:

In each case we have a Task that will resolve successfully with an a value or unsuccessfully with an x value.

package.elm-lang.org/packages/elm/...

As I suspected, coming back to this project after having slept well, things make a lot more sense to me now.