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