Elm 0.18: How to scroll a (non-window) element?


So, recently we’ve released the first Beta version of Planga, a Seamless Chat Integration Service that you can use to add live chat to your application. It will use your users and your defined channels, but other than that, it will get out of the way of your application, so that you do not have to worry about the messy details of building a scalable, feature-rich chat system at all.

Planga’s back-end is built using Elixir, and the front end is currently a very simple JavaScript class.

Needless to say, because this front-end interface is going to be built upon a lot in the future, we decided that it was time to re-build the initial JS version into an Elm application.

Long story short: The Elm app almost has reached feature parity with the JS version. The only thing that is still missing and non-trivial to add, is proper handling of the scrollbar.

This is the desired behaviour (this is very common, natural and expected behaviour for chat interfaces):

  1. When a person scrolls to the top, older messages should be loaded. Once they are added to the interface, the scrollbar should remain at the same distance from the bottom of the chat window. (in JS terms, newScrollTop = newScrollHeight - (oldScrollHeight - oldScrollTop). See the lines in the JS code that does this: The user should be able to scroll further to the top now, but still see the same messages as before.
  2. When the scrollbar is near or at the bottom, whenever a new message arrives, the scrollbar should move all the way to the bottom, so the message is visible without having to manually scroll.

However, I cannot figure out a way to do this with Elm, even with ports, because the code in a port has no idea when Elm has updated its (V)DOM (and then the real DOM), so it cannot properly add listeners and respond to changes in the view: We need to both listen to scroll events, as well as being aware of the current scroll position and inner height (the scrollHeight) of the element rendered by Elm.

The couple of libraries out there that let Elm immediately react to scroll events only work with the scrolling of the window not of an element that is rendered by Elm itself. (in this case: the list of chat messages).

IIRC Elm 0.19 has ways to set up scroll listeners to elements. However, because the chat application uses Phoenix and websockets, we’re stuck on 0.18 for now.

So, we’ve reached the point where it seems reasonable to dig into the internals and write some Native code to fix this problem (until we can migrate over to 0.19 and throw it away). Any solution that does not involve setTimeout/setInterval over ports (because that stuff is highly dependent on how fast a device is able to render and therefore not portable and prone to race conditions) is accepted.

We would be very grateful for your help. :heart:


Any chance you can know the height of a specific item of input (e.g. depending on line size, smileys, other specific content) ? Or are all your items of input of the same size ?

If so, it shouldn’t be too hard to calculate the total height of your loaded data, and integrate new lines nicely using dom.scroll. That’s what we intend to do for horizontal scroll in our planning. Once we get the time. Some day.

Also unrelated to your question, but as for relocating the scrollbar nicely when new data is loaded, the conclusion I have come to is that the traditional scrollbar model sucks for infinite content. It sucks in a way that is not problematic when you use touch or mousewheel, beause you don’t control specifically the position of the scroller, but on mouse click control, it sucks because reaching a loading zone while the scrollbar is locked by mouseclick will either result in a jump, or break mouse control.

My idea to fix this is to make mouse control of the scrollbar work by giving momentum rather than controling position. If you need the ability to do both, you should have a scrollbar that signals this affordance in some clever way.


Unfortunately not, since the code is given to users (web-application developers) that are freely able to alter the CSS styling the Elm app appears in. (And besides this, the size also differs greatly based on device size, etc.)


The obvious place to start might be to look at the 0.19 code that does this (Browser.Dom.get/setViewportOf). Seems like there might be a good chance that code could mostly be lifted as-is and dropped into 0.18?


After fiddling with native code somewhat, it turns out that the elm/browser library is the one that superseeded the elm-lang/dom one. It offers similar functions (different names, but same functionality) to fetch the scroll position and set it to an arbitrary value, as well as top and bottom, for any element whose HTML ID you know. :smiley:


So I just continued working on this. I ended up copying the native scroll-related code of the elm-lang/dom library and added two calls to it:

  • one to fetch the current vertical scroll position, as measured from the bottom.
  • one to set the current vertical scroll position, relative to the bottom.

Now Elm does not have to be concerned with the fact that in JS these do not really exist by themselves at all!

The resulting code contains two tiny native functions, zero usage of ports and zero race conditions.
Thank you very much, everyone! I would not have been able to solve this problem without your help.