Browser.Dom

This module allows you to manipulate the DOM in various ways. It covers:

  • Focus and blur input elements.
  • Get the width and height of elements.
  • Get the x and y coordinates of elements.
  • Figure out the scroll position.
  • Change the scroll position!

We use different terminology than JavaScript though...

Terminology

Have you ever thought about how “scrolling” is a metaphor about scrolls? Like hanging scrolls of caligraphy made during the Han Dynasty in China?

This metaphor falls apart almost immediately though. For example, many scrolls read horizontally! Like a Sefer Torah or Chinese Handscrolls. The two sides move independently, sometimes kept in place with stones. What is a scroll bar in this world? And hanging scrolls (which are displayed vertically) do not “scroll” at all! They hang!

So in JavaScript, we start with a badly stretched metaphor and add a bunch of DOM details like padding, borders, and margins. How do those relate to scrolls? For example, JavaScript has clientWidth. Client like a feudal state that pays tribute to the emperor? And offsetHeight. Can an offset even have height? And what has that got to do with scrolls?

So instead of inheriting this metaphorical hodge-podge, we use terminology from 3D graphics. You have a scene containing all your elements and a viewport into the scene. I think it ends up being a lot clearer, but you can evaluate for yourself when you see the diagrams later!

Note: For more scroll facts, I recommend A Day on the Grand Canal with the Emperor of China or: Surface Is Illusion But So Is Depth where David Hockney explores the history of perspective in art. Really interesting!

Focus

focus : String -> Task Error {}

Find a DOM node by id and focus on it. So if you wanted to focus a node like <input type="text" id="search-box"> you could say:

import Browser.Dom as Dom
import Task

type Msg
    = NoOp

focusSearchBox : Cmd Msg
focusSearchBox =
    Task.attempt (\_ -> NoOp) (Dom.focus "search-box")

Notice that this code ignores the possibility that search-box is not used as an id by any node, failing silently in that case. It would be better to log the failure with whatever error reporting system you use.

blur : String -> Task Error {}

Find a DOM node by id and make it lose focus. So if you wanted a node like <input type="text" id="search-box"> to lose focus you could say:

import Browser.Dom as Dom
import Task

type Msg
    = NoOp

unfocusSearchBox : Cmd Msg
unfocusSearchBox =
    Task.attempt (\_ -> NoOp) (Dom.blur "search-box")

Notice that this code ignores the possibility that search-box is not used as an id by any node, failing silently in that case. It would be better to log the failure with whatever error reporting system you use.

type Error
= NotFound String

Many functions in this module look up DOM nodes up by their id. If you ask for an id that is not in the DOM, you will get this error.

Get Viewport

getViewport : Task x Viewport

Get information on the current viewport of the browser.

getViewport

If you want to move the viewport around (i.e. change the scroll position) you can use setViewport which change the x and y of the viewport.

type alias Viewport =
{ scene : { width : Float, height : Float }
, viewport : { x : Float, y : Float , width : Float , height : Float }
}

All the information about the current viewport.

getViewport

getViewportOf : String -> Task Error Viewport

Just like getViewport, but for any scrollable DOM node. Say we have an application with a chat box in the bottow right corner like this:

chat

There are probably a whole bunch of messages that are not being shown. You could scroll up to see them all. Well, we can think of that chat box is a viewport into a scene!

getViewportOf

This can be useful with setViewportOf to make sure new messages always appear on the bottom.

The viewport size does not include the border or margins.

Note: This data is collected from specific fields in JavaScript, so it may be helpful to know that:

Neither offsetWidth nor offsetHeight are available. The theory is that (1) the information can always be obtained by using getElement on a node without margins, (2) no cases came to mind where you actually care in the first place, and (3) it is available through ports if it is really needed. If you have a case that really needs it though, please share your specific scenario in an issue! Nicely presented case studies are the raw ingredients for API improvements!

Set Viewport

setViewport : Float -> Float -> Task x {}

Change the x and y offset of the browser viewport immediately. For example, you could make a command to jump to the top of the page:

import Browser.Dom as Dom
import Task

type Msg
    = NoOp

resetViewport : Cmd Msg
resetViewport =
    Task.perform (\_ -> NoOp) (Dom.setViewport 0 0)

This sets the viewport offset to zero.

This could be useful with Browser.application where you may want to reset the viewport when the URL changes. Maybe you go to a “new page” and want people to start at the top!

setViewportOf : String -> Float -> Float -> Task Error {}

Change the x and y offset of a DOM node’s viewport by ID. This is common in text messaging and chat rooms, where once the messages fill the screen, you want to always be at the very bottom of the message chain. This way the latest message is always on screen! You could do this:

import Browser.Dom as Dom
import Task

type Msg
    = NoOp

jumpToBottom : String -> Cmd Msg
jumpToBottom id =
    Dom.getViewportOf id
        |> Task.andThen (\info -> Dom.setViewportOf id 0 info.scene.height)
        |> Task.attempt (\_ -> NoOp)

So you could call jumpToBottom "chat-box" whenever you add a new message.

Note 1: What happens if the viewport is placed out of bounds? Where there is no scene to show? To avoid this question, the x and y offsets are clamped such that the viewport is always fully within the scene. So when jumpToBottom sets the y offset of the viewport to the height of the scene (i.e. too far!) it relies on this clamping behavior to put the viewport back in bounds.

Note 2: The example ignores when the element ID is not found, but it would be great to log that information. It means there may be a bug or a dead link somewhere!

Position

getElement : String -> Task Error Element

Get position information about specific elements. Say we put id "jesting-aside" on the seventh paragraph of the text. When we call getElement "jesting-aside" we would get the following information:

getElement

This can be useful for:

  • Scrolling — Pair this information with setViewport to scroll specific elements into view. This gives you a lot of control over where exactly the element would be after the viewport moved.

  • Drag and Drop — As of this writing, touchmove events do not tell you which element you are currently above. To figure out if you have dragged something over the target, you could see if the pageX and pageY of the touch are inside the x, y, width, and height of the target element.

Note: This corresponds to JavaScript’s getBoundingClientRect, so the element’s margins are included in its width and height. With scrolling, maybe you want to include the margins. With drag-and-drop, you probably do not, so some folks set the margins to zero and put the target element in a <div> that adds the spacing. Just something to be aware of!

type alias Element =
{ scene : { width : Float, height : Float }
, viewport : { x : Float, y : Float , width : Float , height : Float }
, element : { x : Float, y : Float , width : Float , height : Float }
}

A bunch of information about the position and size of an element relative to the overall scene.

getElement