Scroll and Resize events in Elm

Demo: https://lucamug.github.io/elm-scroll-resize-events/

This is a small example about scroll and resize events in Elm through ports.

I created two ports in Elm, one for receiving data about the scroll position, the page height and the viewport dimension. These are all integer numbers as described in this type definition

type alias ScreenData =
{ scrollTop : Int
, pageHeight : Int
, viewportHeight : Int
, viewportWidth : Int
}

The port is simply defined as

port scrollOrResize : (ScreenData -> msg) -> Sub msg

The main script subscribes to this port with

subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ scrollOrResize OnScroll
]

So now every time Javascript send data through this port an OnScroll message is sent to the update function.

Then update function just update the model with the data:

        OnScroll data ->
( { model | screenData = Just data }, Cmd.none )

The Javascript has a small function for throttling and debouncing the scroll and resize events. Without throttling and debouncing these events are firing many time every seconds and can degrade the user experience.

The function is

var scrollTimer = null;
var lastScrollFireTime = 0;
var minScrollTime = 200;
var scrolledOrResized = function() {
if (scrollTimer) {} else {
var now = new Date().getTime();
if (now - lastScrollFireTime > (3 * minScrollTime)) {
processScrollOrResize();
lastScrollFireTime = now;
}
scrollTimer = setTimeout(function() {
scrollTimer = null;
lastScrollFireTime = new Date().getTime();
processScrollOrResize();
}, minScrollTime);
}
};

The first if condition if (scrollTimer) check if there is an initialised timer (setTimeout). In that case the function just exit.

The second if condition if (now — lastScrollFireTime > (3 * minScrollTime)) check if long time is passed since last time the function was called. If this is true, this is probably the initial moment of a new scrolling action so we want to send data to Elm right away and then set the timer.

The final section is the timer setting. This will fire after a certain period of time and during that period the function will not take any action, due to the first if condition.

The function that is used to communicate to Elm is

var processScrollOrResize = function() {
var screenData = {
scrollTop: parseInt(_window.pageYOffset || _html.scrollTop || _body.scrollTop || 0),
pageHeight: parseInt(Math.max(_body.scrollHeight, _body.offsetHeight, _html.clientHeight, _html.scrollHeight, _html.offsetHeight)),
viewportHeight: parseInt(_html.clientHeight),
viewportWidth: parseInt(_html.clientWidth),
};
app.ports.scrollOrResize.send(screenData);
}

This function use some trickery to get data out of different browsers. Then send this data through the port scrollOrResize.

After the data is received by Elm and the update function updated the model, the new view is rendered. The view use the new data to render a bar at the top with the percentage of scrolling and a modal in the centre with the data.

There is another port in Elm that is used to scroll the page to the top

port scrollTop : Int -> Cmd msg

Javascript subscribe to this port with

app.ports.scrollTop.subscribe(elmScrollTop);

That would simply execute this function

var elmScrollTop = function(position) {
_window.scroll({
top: position,
left: 0,
behavior: 'smooth'
});
};

window.scroll is in a Working Draft stage so I load a polyfill for it.

To run the demo locally you can execute the following commands:

$ git clone https://github.com/lucamug/elm-scroll-resize-events
$ cd elm-scroll-resize-events
$ elm-live --output=elm.js src/Main.elm --open --debug