Race Condition? Fetching an element by ID of a view which is yet to be rendered

Hello, I’m trying to draw some simple SVG in a certain view. To properly draw this SVG I need to know the available width of a particular element. To achieve know the width I rely on Browser.Dom.getElement, via Task.attempt. However, I believe I’m incurring into a race condition where I send the Cmd to fetch the element, but the view may not be rendered yet, thus the Cmd fails. Ideally, this Cmd would understand that it has to wait until the new view is rendered before looking for the element.

In my particular project the only way to mitigate the issue I thought about is in the update function. As I receive the Msg with the Result from the Cmd, if it has failed then I send it again, and keep retrying until it works. Although I think this could work, I’d rather have a more clean solution.

I’ve developed this Ellie, which doesn’t exactly mimic my project, but shows the problem. As you see when you compile there’s a brief flash of a red background which is rendered when the Cmd failed, and then later turns green when it succeeds.

Hi, its not a race condition as such - that is when two threads make competing updates. Javascript in the browser is single threaded, so there can be no race conditions. I can see what you describing though, it takes a little time for the view to render, and you would like to know when an element has been rendered so that you can ask for its size.

I think what you need is a MutationObserver: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

You could hook that up through ports, to listen for when your element has been added to the DOM?

You could also try onAnimationFrame: https://package.elm-lang.org/packages/elm/browser/latest/Browser-Events#onAnimationFrame. If you wait at least until the next animation frame before making your first request to get the element, it is more likely to be present, but I don’t think it is guaranteed to be present, so you might still have to keep trying until you get it.

I think the MutationObserver approach is going to be more robust - as far as I know there is no kernel package that wraps it so you’d need the ports.

I think you could also build some custom HTML element that fires an event when your element is added to it? Then listen to that event through Html.Events.on: https://package.elm-lang.org/packages/elm/html/latest/Html-Events#on

1 Like

Whenever I draw SVG, I also like to know the size of the element being drawn into. This is so that I can set the SVG viewport size to exactly match its pixel size, which makes it easier to align drawings to an integer number of pixels. At the moment all my SVG applications are works in progress and full-screen, so I query for the window size and only render the SVG after that.

I will need to change them to render within an element, and also to adapt whenever that element changes size - as these applications mature there will be more stuff on the screen around where the SVG rendered diagrams will go.

So I am also interested in figuring out a good solution for this.

1 Like

@rupert after some thought I’m going to try something like an Element Subscriber, which receives a list of element IDs to subscribe and then may use onAnimationFrame to determine which of those elements are currently rendered. Possibly the element to look for could be filtered based on the URL. So each element would have an associated URL, and if the user isn’t on that URL at the moment it won’t search for that element.

This is just a thought, I think I’ll give it a quick shot at implementing this.

Another possibility may be inspired from this post. The user says the following:

The focus function in the elm-lang/dom package is used to set focus with a Task (without using any port s or JavaScript).

Internally it uses requestAnimationFrame to ensure any new DOM updates are rendered before it tries to find the DOM node to focus on.

So if focus really waits for new DOM updates to be rendered before trying to the DOM node to focus on then maybe we could use this function to ensure the node has been rendered and then use getElement to find its width?

Just a heads up, the functions in Browser.Dom all use requestAnimationFrame, so theoretically when they run any elements added in the next view call have been rendered. Source here: https://github.com/elm/browser/blob/master/src/Elm/Kernel/Browser.js

1 Like

@Sidney that’s very interesting. However, if that’s the case then my example shouldn’t flash to red and then turn green, right?

The user @jessta commented on Slack that if on my project it’s sometimes succeeding and other times failing, then this means there’s a bug in how getElement is implemented?

If this truly is the case where could I open an issue for this?

I don’t think the view always renders on the very next animation frame. For example, if preceding update or view functions are taking too long, the next frame request will miss the very next frame and likely get one after that. You can expect things to take a little longer when an application is starting up, so that could explain it.

The initial red flash is the first paint, the first frame. On the next frame, the element size is measured and update gets called. Then on the next frame (the third frame?), the view is rendered again, green now.

Here’s a snapshot from the Chrome profiler, each red square is one animation frame callback:

image

I guess you could make the element visibility: hidden when the size isn’t known, so that you don’t see the element until the size has been calculated.

2 Likes

I was thinking about this. When the first version of the model is returned from init, the view can be rendered from that, and that must be happening before the getElement is run. Basically, exactly what @Sidney’s screen shot shows. :+1:

Would it be useful to have some way in the Browser API, to subscribe to getElement in addtion to it being a Task you have to explicitly request each time you want it:

https://package.elm-lang.org/packages/elm/browser/latest/Browser-Dom#getElement

Something along the lines of the onResize subscription:

https://package.elm-lang.org/packages/elm/browser/latest/Browser-Events#onResize

But to subscribe to the element being resized - like a ResizeObserver. (Or to subscribe to something like the Element data structure, whenever the element is resized or perhaps by other events in its lifecycle, such as being added/removed to the DOM)

module Browser.Events exposing ...

{-| The first parameter is the element id to subscribe to resizes of.
-}
onElementResize : String -> (Int -> Int -> msg) -> Sub msg

This would be useful for SVG because after getting the initial size of the element to draw into, you might want to listen for changes to its size. I am tending to make the SVG viewport resize to match its containing element, and keep the pixels and viewport in a 1:1 relationship.

The above is what I have implemented already as a port in my application.

I’ve been doing some experiments with onAnimationFrame and so far a very clear downside is that the Elm Debugger becomes unusable if you subscribe to onAnimationFrame… receiving 60 messages per second…

@rupert I think it would be useful to have something like onElementDrawn to be notified when a particular node was rendered on the page. However, I’m not sure if the onResize subscription for an element would be so useful because if you resize the window then the element will either:

  1. Stay the same size because the document isn’t responsive or hasn’t adapted to the change yet.
  2. If it has adapted then you can fetch the element dimensions again by relying on the onResize subscription of the window.

Just a thought :thinking:

Anyway, I’ll probably keep looking into this issue this week, trying to find a good solution.

You could also have a situation where the window size does not change, but an element within it does. Like if my SVG drawing area was next to a tool bar that could be resized and the drawing area would be resized to fill the remaining space, for example.

I think onElementDrawn could work well as a subscription too. Or perhaps something that exposes a bit more of what a mutation observer can report - elements added or removed.

1 Like

You’re right! Didn’t even think of that.

You mentioned:

The above is what I have implemented already as a port in my application.

Do you think you could show your implementation? I’d possibly give it a go at implementing an onElementDrawn function using ports.

Been busy, but here it is.

The javascript code:

const sizeObserver = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    $('.watch-resize').each(function() {
      var result = {
        id: this.id,
        height: this.clientHeight,
        width: this.clientWidth
      };

      app.ports.Ports.resize.send(result);
    });
  });
});

sizeObserver.observe(topElement, {
  childList: true,
  subtree: true
})

The Elm code:

port resize : (ResizeEvent -> msg) -> Sub msg

type alias ResizeEvent =
    { id : String
    , width : Float
    , height : Float
    }

This is by no means a complete resize observer - it is just watching for mutations and sending up the new size every time one occurs, and there are plenty other ways in which the size of something can be changed.

You should be able to take a shot at writing onElementDrawn based on a MutationObserver though.

1 Like

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