How to avoid rerendering of a div that moves around the page?

I’m working on an app with a text editor and a preview of the output from what you’ve typed in. There are options for the user to change the app’s layout in ways that move that preview to different parts of the page. The preview is expensive to render in terms of both time and resources.

Is there a way in Elm to accomplish something like Html.Keyed, where the same item in a different position isn’t rerendered, but for something that isn’t just changing position in a list, but moving around to disparate parts of the DOM?

So far the best I’ve gotten is to render it in every part of the DOM where it could appear, but to display: none the ones that aren’t currently active. This works (for the most part), but is more expensive than I’d like. Wondering if there are other/better tricks for this?

Wouldn’t Html.lazy do the trick?

I haven’t used lazy before, but isn’t it for avoiding recalculating the virtual dom for the same element in the same place? I’ve been assuming it wouldn’t apply here since even though it would be the same arguments to the same function, it’s not in the same part of the dom.

You could possibly do this

Html.div 
    [ -- Html.Attributes that offset the child element to the desired position.
    ]
    [ Html.lazy ... ]

You probably want to use CSS to change the position rather than DOM position. For example the order property in flex box or CSS grid should help you

2 Likes

I have a similar situation for completely different reasons. In my case I have an elm element that exposes ports to bridge communication with a service. This is how I attach it to elm and have it persist on changes of layout:

  1. Make the preview pane be a different elm app that you can embed and that exposes ports to tell it what to render.
  2. In the main app, have an empty div [id "preview-pane"] [] that you can recreate anywhere else when you want.
  3. Get a reference to the #preview-pane from javascript.
  4. Create a div that you add as a child of #preview-pane and attach the preview pane elm app to this child div.
  5. Have a javascript timeout that keeps reattaching the preview pane DOM node to the #preview-pane whenever it becomes orphan.

I am using it successfully. This is what my JavaScript code does, refactored to match your example:

mainApp.ports.mainAppReady.subscribe(function() {
    const placeHolder = document.getElementById("preview-pane");
    if (!placeHolder) {
        console.error("No div with id 'preview-pane' was found.");
        return false;
    }
    const childDiv = document.createElement("div");
    placeHolder.appendChild(childDiv);
    const client = new Elm.PreviewPane.init({
        node: childDiv,
        flags: {
        }
    });
    let elmNodes: any = [];
    setInterval((): void => {
        if (elmNodes.length == 0) {
            const placeHolder = document.getElementById("preview-pane");
            elmNodes = placeHolder ? placeHolder.children : [];
        }
        if (elmNodes && !document.body.contains(elmNodes[0])) {
            const placeHolder = document.getElementById("preview-pane");
            if (!placeHolder) {
                console.warn("No div with id 'preview-pane' was found.");
            } else {
                placeHolder.appendChild(elmNodes[0]);
            }
        }
    }, 500);
    return true;
});

1 Like

Woah, cool, I would never have thought of that! Definitely going to try this. Thanks!

Personally I would just use CSS Grid to ensure that your preview is always the same div (always the same relative location in your rendered HTML source) and allow the CSS to re-layout your div to wherever in the screen’s layout it needs to be shuffled. :slight_smile:

Appreciating the CSS placement suggestions, but I’m using elm-ui for layout and not sure how to make that play nicely with grid without rewriting a looot of the view code and possibly switching away from using elm-ui which I overall really like.

Ah, I feel you there. elm-ui is amazing. francescortiz’ solution may be the best in that case. :+1:

Although… another possible elm-ui-centric solution might be to render your preview pane above/in-front-of the whole-page container, then change it’s positioning and size as needed to put it into the hole left in the layout to accommodate it. Optionally you could put a simple FPO (for positioning only) object into the hole and monitor the position/size of that, feeding the results into the preview pane’s properties.

In the long term, maybe one day @mdgriffith could work out some CSS Grid backend goodness for elm-ui, and/or TEA could perfect a concept of identifiers to help the diff process to relocate large chunks of HTML/vDOM from one part of the larger tree to another without it’s having to be fully recalculated or recreated. That would be fun. :slight_smile:

1 Like

I think this would be much better for this use case.

I’ve done this in Elm previously. It works but one thing you notice is that between the events giving you the position of the FPO, and the next view render that puts the preview widget in place, there does tend to be a noticeable lag - probably just 1 animation frames worth. Without an immediate call to get position this is never going to be possible to do within a single animation frame. Its not the end of the world, just grates a little when you are taking so much care to make everything else work smoothly for a great user experience.

A way I can think to avoid this blinking issue is to have a design that you can predict and instantly calculate the new position and apply it immediately. No FPO and no dom querying involved, only Elm.

I am not sure this will help you, but I had a similar use case in some app where I wanted to decide when to re-render some part of the DOM which was contenteditable.

I ended up storing in my model a boolean that I would toggle True or False just as a way to decide if I should re-render or not.

The relevant part of the view:

...
, if resetView then
    viewDoc article.editableDoc

 else
    Html.div [] []
, if not resetView then
    viewDoc article.editableDoc

 else
    Html.div [] []

Notice that I am repeating myself on purpose to force the re-render.
The following would not do what I want, obviously. In this case it would never re-render:

...
, if resetView then
    viewDoc article.editableDoc

 else
    viewDoc article.editableDoc

Is this relevant to what you’re trying to do?

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