Calculating the height of the horizontal scrollbar: really need to call JavaScript

Hi All,

For elm-mdc I need to know the height of the horizontal scrollbar as this needs to be set as negative margin padding on the tab bar scroller element. This value differs per browser, and probably per OS as well.

In MDC they calculate this in a piece of typescript:

export function computeHorizontalScrollbarHeight(documentObj: Document, shouldCacheResult = true): number {
  if (shouldCacheResult && typeof horizontalScrollbarHeight_ !== 'undefined') {
    return horizontalScrollbarHeight_;
  }

  const el = documentObj.createElement('div');
  el.classList.add(cssClasses.SCROLL_TEST);
  documentObj.body.appendChild(el);

  const horizontalScrollbarHeight = el.offsetHeight - el.clientHeight;
  documentObj.body.removeChild(el);

  if (shouldCacheResult) {
    horizontalScrollbarHeight_ = horizontalScrollbarHeight;
  }
  return horizontalScrollbarHeight;
}

So they insert an element, calculate the scrollbar height, and remove the element. Not something that’s easy to do in Elm. If only I could call that piece of JavaScript! Its a pure function, basically a constant. But I can’t call it.

So what are my options?

Perhaps I could use a port (I’m afraid we will see a screen flash before I have the correct value). But Evan says you can’t use ports in libraries. So that would not help me.

Perhaps I can tell users of the library to pass in the value via flags, but that would make the library less user-friendly, and not help with anything Elm tries to achieve (just make life harder).

The final option would be custom elements but I’m afraid that would have the same issues as ports: the browser may decide to paint the screen with the incorrect css margin causing a flash on the next animation frame.

I just want to call this javascript function. That’s all I’m asking. Please! :slight_smile:

I’m eagerly awaiting your collective wisdom.

1 Like

Maybe you can do the same in Elm… ?

Make a custom type like type alias ScrollbarHeight = Testing | Tested Int

view =
Case Scrollbarheight of
Testing ->
addTemporaryimage
Tested height ->
renderStuff height

In the Testing branch you can attach an Elm Html onload event to a temporary img element. Then decode the scrollbar values from that onload event and change ScrollbarHeight to Tested in your update function.

I have not tested this :slight_smile:

My first thought was using Browser.Dom to inspect an element, but unfortunately Browser.Dom does not expose offsetHeight/offsetWidth.

From getViewportOf:

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!

I’m not really sure what’s meant by “the information can always be obtained by using getElement on a node without margins”.

Here’s what I’m thinking:

  • Calculate height in JavaScript before starting Elm app
  • Pass in via a flag
  • Components that need the height expect it as an argument or as part of a config record
1 Like

I think it’s referring to viewport.height. But element.height will include the height of the scrollbar. So, yes, I think you can use Browser.Dom.getElement for what you are trying to do.

@Hector good point. Actually, Browser.Dom.getElement won’t tell you the “inner size”, but you can combine it with Browser.Dom.getViewportOf (which only gives you the inner size) to figure out the scrollbar’s width/height.

Here’s a demo: https://ellie-app.com/5hcrXSJCzKSa1
(It checks for a vertical scrollbar but it’s easy enough to adapt to look at the horizontal scrollbar).

You could extract it into a function that returns a Task:

getScrollbarWidth : String -> Task Browser.Dom.Error Float
getScrollbarWidth elementId =
    Task.map2
        (\element viewport -> element.element.width - viewport.viewport.width)
        (Browser.Dom.getElement elementId)
        (Browser.Dom.getViewportOf elementId)

Of course, that depends on having an element in the DOM to look at. You can run it in init (as long as the element is rendered on the first call to view, but you’ll still be missing that info for one animation frame.

1 Like

That’s what I settled on. But it makes the library harder to use. And you have to drag what is basically a constant around to every page.

What about the Material.Model that has to be passed to every component? :wink:

The scrollbar width is dependent on the environment, so it’s a side effect to retrieve it, right?. It has to be stored as state just like anything else. I wonder if it could be included in the Material.Model somehow.

With the current code quite hard, i.e. I didn’t see a way to do that. I don’t think you can get it in defaultConfig in TabBar/Implementation.elm. So currently you pass it in as config.

The other option would be the pass it in as the initial model for the tab bar. But I couldn’t get getSet to work. It should read like for example:

getSet height =
    Component.indexed .tabbar (\x y -> { y | tabbar = x }) (defaultModel height)

Couldn’t make that work.

I.e. the part of getting it in the global Mdc model is easy, but getting it to flow into the tab bar has stumped me so far.

Going another way with it, could you include the code for calculating height in elm-mdc.js, and then write it out in as a CSS variable that can then be used (hackily) from Elm? If you include the attribute "style" "margin-bottom: var(--scrollbar-height)" first in the attribute list for the element it won’t clobber other style attributes.

6 Likes

That’s brilliant! MDC could even adopt that technique. Will give this a go!

1 Like

I have now implemented this technique in elm-mdc, and it’s working great! Thanks again for this great suggestion.

1 Like

Excellent, glad it worked!

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.