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
- 2. Responding to window resize events
- 3. The code
- 4. Conclusion
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
}
}
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
^^^^^^^^^^^^^^^^^^^^^^^
ThisgetViewport
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
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
^^^^^^^^^^^^^^^^^^^^
ThisGetBrowserDimensions
value is a:
Msg
Butperform
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 performarkham 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#onResizearkham 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/bZC6k6dv9wna1arkham 1 hour ago
I also converted the Ints to Floats to get the type checker to be happyarkham 1 hour ago
and here’s a very simple example of a subscription: https://guide.elm-lang.org/effects/time.htmlarkham 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 )
The model is straight forward. Although GotInitialViewport
and Resize
look different, they both involve dealing with two Float
s.
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 ))
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 Msg
s 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 )
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"
)
]
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)
When you say
I completely agree. It would be a lot better to change from
GotInitialViewport Viewport
toGotInitialViewport (Result x Viewport)
(I'm not entirely sure what the type of the error is there). This moves the error handling to yourupdate
instead of yourinit
.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.
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 unsuccessfulTask
execution: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.