HTML onLoad not available natively?

I need to measure part of my view (using Browser.Dom.getElement). This can, of course, only happen once the view is rendered. However, the custom ‘on’ function of the html event system is (apparently) not working for onLoad:

view : Model -> Html Msg
view model =
   Html.div [Html.Events.on "load" (Decode.succeed DoOnLoadStuff)]

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
   case msg of
      DoOnLoadStuff ->
         Debug.log (Debug.toString "In DoOnLoadStuff")
         ( model, getTextSize )

In update, the code following DoOnLoadStuff never runs (and there is no log to the console).
However, when I replace "load" with "click", a click does send the message, and the update code is run.

onLoad is clearly ‘non-passive’ by a naive definition, since it fires without user interaction, but I don’t think it satisfies ‘non-passive’ by the Passive event listeners definition.

There is an issue posted about this specifically for elm-ui and images:
Custom onLoad event handler does not work for images #14
but the problem seems to be deeper than that.

My questions:

  1. Am I doing something wrong?

  2. Is there an Elm native way to ‘do something when the view is ready’ ?

  3. More broadly, what options are there to make stuff happen without user interaction?

All packages in the test system are latest at time of writing:

    "elm-version": "0.19.0",
    "dependencies": {
        "direct": {
            "elm/browser": "1.0.1",
            "elm/core": "1.0.2",
            "elm/html": "1.0.0",
            "elm/json": "1.1.3",
            "mdgriffith/elm-ui": "1.1.0"
        "indirect": {
            "elm/time": "1.0.0",
            "elm/url": "1.0.0",
            "elm/virtual-dom": "1.0.2"
1 Like

Hello :sunny: The load-event is fired from the document itself, not its elements.

Have you tried getting the viewport of the element from the init-function?

Also, what is getTextSize doing exactly?

Thank you for your reply, @opvasger

The load-event is fired from the document itself, not its elements.

Are you sure?

According to MDN Global​Event​Handlers​.onload

“The onload property of the GlobalEventHandlers mixin is an EventHandler that processes load events on a Window, XMLHttpRequest, <img> element, etc.
The load event fires when a given resource has loaded.”

And while – onload Event does say…

“onload is most often used within the <body> element to execute a script once a web page has completely loaded all content (including images, script files, CSS files, etc.).”

…it does also give this example:

<img src="w3javascript.gif" onload="loadImage()" width="100" height="132">

function loadImage() {
  alert("Image is loaded");

So I was assuming that it should fire on a div once the div and all its content has loaded.

But maybe I’m misinterpreting.

Have you tried getting the viewport of the element from the init-function?

That’s what I tried first. And it would be ideal. Unfortunately, commands launched by init execute too soon: before the view is rendered.

Also, what is getTextSize doing exactly?

It’s using Browser.Dom.getElement to get the size of a piece of text in the view.

I think you may be misunderstanding what the load event does on elements. From

The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets images.

The load event is not for when an element is rendered, but for when the whole page loads initially, way before elm runs, renders and it binds it’s handlers.

I’m not sure how to do what you need exactly with just Elm cleanly. Here are some options I could think of:

  • After a request animation frame things should be rendered except maybe for the initial init/ update cycle where that doesn’t seem to hold, and you have to handle conditional subscriptions with some model flag (which there is a bug with right now if you do so on init, I think).
  • Another option could be Process.sleep for some milliseconds and then trying to measure.

If you don’t mind using JS,

  • you can send a port message after your update function branch runs, and then requestAnimationFrame in JS and respond.
  • You could also make a custom element that triggers a custom event on connectedCallback, which I think is when it is added to the DOM, and listen for it in Elm HTML.
1 Like

I also forgot to mention that CSS is very powerful so depending on what you want to accomplish, it may be doable without resorting to manually measuring elements. It may be that this is a XY problem situation. If you want, mention what you are actually trying to accomplish and we can try to find alternatives with you :slight_smile:

There is a load event for the document, frames, images, and scripts, but not for div or other elements.

For cases where we want to measure some things size we tend to use custom elements, you can use the connectedCallback in them to know when they are mounted to the DOM and send an event then. For values that will change over the life of the element a MutationObserver or IntersectoinObserver inside the custom element are useful.

1 Like

What do you need to measure the text for?

I’ve attached load listener to divs in a few JavaScript projects to say: “If an image in here loads, update some measurements”. (Images that load typically cause layout changes.)

I’m not sure why but I recalled seeing somewhere some time ago that the Browser effects are run after requestAnimationFrame so that you could use them assuming the render of that update cycle happened.

I’m not sure if it is exactly true but I’ve made an Ellie and just triggering getElement works fine for what you described:

So it seems like this is not an issue?

1 Like

Thank you also to @joakin, @antew and @lydell for your replies.

That’s what I want :slight_smile:

Naively I was thinking that the whole page hasn’t rendered until the view has rendered.

But, of course, Elm generated html is a manipulation of the DOM after the initial html has rendered (which, for most of us, is little more than a pair of body tags and a script call).

So, the load event fires after the JS has loaded but before any JS has executed. Which means that the load event has already fired before the JS (from Elm) is listening for it. Only JS embedded in an html tag can catch it’s load event.

Thank you for your suggested solutions, @joakin. I am happy to use some JS, but was hoping there was an Elm native way to do it.

I had considered onAnimationFrame and Process.sleep, but felt that a ‘wait-a-bit, try-now’ cycle isn’t a very elegant or reliable solution.

That would explain why it won’t fire on a div. I’m starting to feel a bit of a div myself.

I have a specific requirement but would also like to understand this more broadly.

Specifically, I would like to measure a piece of monospace text, calculate the width to font-size ratio, and use this to make other text fit exactly inside boxes of a given width. I seem to have a solution now, see below.

That’s interesting. So a change in the content of a div can fire an onLoad. But, of course, what changed is still an image, as quoted above by @antew.

That’s brilliant. I’ve been experimenting and have found the same. It would be good to know that it was reliable. Perhaps, as you suggested, a short Process.sleep before the getElement task would make sure it was always successful.
Thank you also for that very neat piece of code in the Ellie.

I’ve done some experimenting following the ideas here and now have a much better understanding. Thanks to all for the great replies. And for your kindness in the face of a not very well thought through question.

1 Like

Yes, events go through capture and bubble phases. All events can be caught in the capture phase, but some events (such as load) do not bubble. In my example I’m capturing load events from any image inside a particular div.

1 Like

Given monospaced text, can’t you calculate the ratio (manually, once) and put it in your code so you can calculate it by using StringLength and FontSize? That’s a lot simpler than asking the DOM in my opinion :sunny:

Yes, that’s the plan. But not all monospace fonts are created equally: the ratio of width (and height) to font size is different for different fonts. And an externally loaded font can be overridden by a local style sheet, or simply fail to load. So the ratios must be calculated each time the app is loaded. The only way I can think of to do this is to measure the screen size, create some text of a reasonable font size (say 5vw), measure the text with Browser.Dom.getElement, and then, as you say, String.length and do the calculations.

It turns out, empirically at least, that the getElement function can be fired by the update branch that gets the initial screen size:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =

   ViewportSize viewport ->
      ( { model | ..., getTextSize )  

   TextSize result ->
      ( { model | 
             textSize = result 
                |> (.element >> .width) 
                |> Result.withDefault 0 }
      , Cmd.none ) 

getTextSize : Cmd Msg
getTextSize =
   Process.sleep 200 
      |> Task.andThen (\_ -> Browser.Dom.getElement "textForSize") 
      |> Task.attempt TextSize

The Process.sleep 200 appears to be unnecessary having tested on Chromium and Firefox. By tested I mean I pressed F5 many times and always got a result :wink:

Thank you for your reply, it sent me on a very beneficial refactoring exercise. Between you and @joakin, my code is now much better :slight_smile:

1 Like

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