hyperfiddle

noonian 2025-04-20T01:19:59.653029Z

Is there a current pattern in v3 for waiting on markup to render before evaluating an expression? I am integrating with a third-party js library that is looking at the dom and I can't get it to wait until everything has rendered

henrik 2025-04-20T10:07:56.803929Z

https://floating-ui.com/ includes facilities for auto updating and repositioning when the location or size of the anchor changes. We use it for our dropdowns. The auto update callback can be wrapped with m/observe.

đź‘€ 1
Dustin Getz (Hyperfiddle) 2025-04-20T12:57:11.994659Z

There is not a clean idiom to determine when the DOM rendering is "done", because the computation, being reactive, is never really done, there can always be more things in flight from the server so it's not a well defined condition. The only robust way to establish this condition that I am aware of is to observe the dom and wait for it to reach a known target state. https://github.com/hyperfiddle/electric/blob/6ba8e2a6073e6fab621a207950a50e2d05bbcf66/src/hyperfiddle/electric_dom3.cljc#L665 may be the API you need for this, it uses a MutationObserver to detect DOM changes. The electric examples app uses this to install certain markdown extensions

2
Dustin Getz (Hyperfiddle) 2025-04-20T13:02:28.364869Z

If the condition is too complex, e.g. "li that contains a button class dropdown-toggle with a sibling ul class dropdown-menu"—which is not that complex, the css selector would be shorter than that sentence—you can locally switch to an imperative rendering strategy and use a classical HTML template to render the skeleton dom for the foreign dependency. Code it in clojurescript, call the function, it's imperative like the foreign js lib is, matching impedance

❤️ 1
Dustin Getz (Hyperfiddle) 2025-04-20T13:08:35.967919Z

we hope to switch to natural, left-to-right lexical mount order for effects, which here would mean by the time the dom/node returns out the tail of the dom/li, the children have had an oppty to mount, and unless i have missed something, the problem would be solved. However, when you add e/server dependencies and latency into the equation, you can't rely on that, so i expect you'll still eventually outgrow this. I think there will always be some impedance mismatch at the boundaries between DAG-based and imperative computations, and the answer is for the imperative code to document explicitly its dependencies and assumptions on its interface. And a css selector expressing "we expect the following structure in the DOM" is a fair and reasonable way to state and make explicit that dependency.

đź’Ż 2
Dustin Getz (Hyperfiddle) 2025-04-20T13:11:17.778329Z

If it becomes a big enough problem, i guess electric could add complexity and take more responsibility for assisting with this problem, but i dont think we are interested in adding complexity like this to the electric side any time soon, especially if there are simple workarounds

Dustin Getz (Hyperfiddle) 2025-04-20T13:12:48.235169Z

For example, we render markdown like this:

(ns ...
  (:require ... #?(:clj [markdown.core :refer [md-to-html-string]])))

(e/defn Markdown [?md-str]
  (e/client
    (let [html (e/server (some-> ?md-str md-to-html-string))]
      (set! (.-innerHTML dom/node) html))))
is it really so bad? We think it's actually quite good! Compare to client-side React.js markdown approaches that add huge weight to get React.js components in the markdown, they are all so heavy and buggy, we just render it on the server, send html over the websocket once, write it in, and then for rich markdown extensions, use dom/Await-element to portal in like this:
(when-some [e (dom/Await-element dom/node "#nav")]
  (binding [dom/node e]
    (Nav essay-config ?essay-filename false)))

👍 2
🆒 2
grounded_sage 2025-04-20T13:36:18.900149Z

Just want to say I appreciate these deep dive responses @dustingetz. As I know how valuable this time is to you. It’s very informative to get insights from core team on tradeoffs around these things.

Dustin Getz (Hyperfiddle) 2025-04-20T13:44:58.579399Z

ty we are always delighted to answer clear technical questions, it is the "debug my thing for me it doesnt work" type of requests that make me cranky

henrik 2025-04-20T13:52:29.091919Z

FWIW, I agree that “finished rendering” is a poorly defined notion, and to be robust you have to adapt to continuous re-rendering. With dropdowns as an example: what if the dropdown box is close to the bottom of the screen, and we need it to open upwards in this case? What if it sits in a scrollable area, and the user scrolls it out of view. If you have responsive design, what if the window changes shape in a way that shifts the size and position etc. etc.

👍 1
noonian 2025-04-20T17:44:31.542029Z

Thank you Dustin! I think dom/Await-element was what I was looking for. This is what I ended up with. I agree the issue is really the UI library not documenting the interface and what aspects of the dom matter. I'm having to figure that out as part of integrating it

(e/defn Dropdown []
  (let [parent dom/node
        node (dom/li
               (dom/props {:class "dropdown"})
               (dom/button
                   (dom/props
                     {:type "button"
                      :class "dropdown-toggle"})
                 (dom/text "Toggle dropdown"))
               (dom/ul
                 (dom/props {:class "dropdown-menu"}))
               dom/node)]
    (when (dom/Await-element parent ".dropdown")
      (new-dropdown node))))

👍 1
Dustin Getz (Hyperfiddle) 2025-04-20T17:57:06.645749Z

i'm not certain if that will be 100% future proof, i think the dom/Await-element is accomplishing what setTimeout 0 was accomplishing in your hacked attempt

Dustin Getz (Hyperfiddle) 2025-04-20T17:58:08.460089Z

Now that I think about it, setTimeout 0 works because the DOM are mounting synchronously here

Dustin Getz (Hyperfiddle) 2025-04-20T17:59:06.985729Z

i think the safe answer is writing a very explicit selector to wait for the expected state

Dustin Getz (Hyperfiddle) 2025-04-20T17:59:47.894359Z

i.e., including .dropdown-menu and .dropdown-toggle in the await selector

👍 1
noonian 2025-04-20T18:28:11.525889Z

I am less interested in this specific integration than in generic patterns I can use with electric

grounded_sage 2025-04-20T01:36:44.728459Z

Can you describe more on what you need to achieve?

noonian 2025-04-20T01:48:29.508589Z

I have a JS API that looks at classes on dom nodes to determine it's behavior. I need to render some markup e.g.

#?(:cljs
   (defn inflate-dropdown [node]
     (js/3rdPartyLib.initializeDropdownLogic node)))

(e/defn Dropdown []
  (let [!node (atom nil) node (e/watch !node)]
    (case (dom/li
            (reset! !node dom/node)
            (dom/props {:class "dropdown"})
            (dom/button
                (dom/props
                  {:type "button"
                   :class "dropdown-toggle"})
                (dom/text "Toggle dropdown"))
            (dom/ul
              (dom/props {:class "dropdown-menu"})))
      (when node
        (inflate-dropdown node)))))
And then call a library fn with the top-level dom node. Right now I think I am calling it before the dom has finished rendering and have not been able to force it to wait naively using case, {} etc. It works if I manually call the method from the console after everything is rendered. I can get a reference to the node, it's not a mechanical issue I'm having. I am just not sure how to tell electric to wait until all branches of an evaluation tree have executed (if that's possible)

grounded_sage 2025-04-20T01:56:31.611469Z

you can return the dom node

(let [node (dom/li ..... dom/node)
  (when node
     (do-thing ..)))

grounded_sage 2025-04-20T01:56:47.052449Z

This can also be put in an atom so you can reach the node from anywhere else.

noonian 2025-04-20T01:56:55.529319Z

Yeah, I have the node. It's just getting called too early is the problem. I updated the snippet

grounded_sage 2025-04-20T01:57:14.657119Z

oh i see

grounded_sage 2025-04-20T01:59:02.758549Z

wouldn't the dropdown occur from a user action?

noonian 2025-04-20T01:59:26.386329Z

Yes, but this initializes the javascript that handles those interactions. I could write them from scratch, but one of the things I really like about electric is how easy it is to integrate with third-party APIs

grounded_sage 2025-04-20T02:03:30.587689Z

(reset! !node dom/node) The node isn't finished here.

noonian 2025-04-20T02:04:18.070689Z

Correct, but further in the tree dom/node will be bound to a different element

grounded_sage 2025-04-20T02:05:10.555329Z

Try

(e/defn Dropdown []
  (let [node (dom/li
               (dom/props {:class "dropdown"})
               (dom/button
                 (dom/props
                   {:type "button"
                    :class "dropdown-toggle"})
                 (dom/text "Toggle dropdown"))
               (dom/ul
                 (dom/props {:class "dropdown-menu"}))
                 dom/node)]
    (when node
      (inflate-dropdown node))))

grounded_sage 2025-04-20T02:06:15.844659Z

You are returning the dom/node of li so you just need to do that at the tail

noonian 2025-04-20T02:07:04.520389Z

Exact same behavior this way

noonian 2025-04-20T02:08:32.407849Z

I think electric parallelizes the expressions so it returns dom/node immediately and the dom/props, dom/button, and dom/ul calls run concurrently to the last (return) expression

noonian 2025-04-20T02:10:26.652049Z

I can tell with logging because when I get the node, if I query for it by props I set then nil is returned, so the props have not been set yet

grounded_sage 2025-04-20T02:11:54.608419Z

Okay. Well this is the pattern I have used in my code. I will defer to others.

noonian 2025-04-20T02:20:38.268519Z

This works but it's a total hack and I'm not really a fan of adding in random timeouts (although even seems to work with a timeout of 0ms)

(e/defn Dropdown []
  (let [node (dom/li
               (dom/props {:class "dropdown"})
               (dom/button
                   (dom/props
                     {:type "button"
                      :class "dropdown-toggle"})
                 (dom/text "Toggle dropdown"))
               (dom/ul
                 (dom/props {:class "dropdown-menu"}))
               dom/node)]
    (when node
      (when-let [v (e/Task (m/sleep 0 :val) nil)]
        (case v
          (new-dropdown node))))))

grounded_sage 2025-04-26T00:35:37.625529Z

I’m upgrading my timeseries rendering chart from v1 to v3 and this conversation was very helpful for that work turns out

❤️ 1