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
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.
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
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
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.
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
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)))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.
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
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.
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))))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
Now that I think about it, setTimeout 0 works because the DOM are mounting synchronously here
i think the safe answer is writing a very explicit selector to wait for the expected state
i.e., including .dropdown-menu and .dropdown-toggle in the await selector
I am less interested in this specific integration than in generic patterns I can use with electric
Can you describe more on what you need to achieve?
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)you can return the dom node
(let [node (dom/li ..... dom/node)
(when node
(do-thing ..)))This can also be put in an atom so you can reach the node from anywhere else.
Yeah, I have the node. It's just getting called too early is the problem. I updated the snippet
oh i see
wouldn't the dropdown occur from a user action?
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
(reset! !node dom/node)
The node isn't finished here.
Correct, but further in the tree dom/node will be bound to a different element
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))))You are returning the dom/node of li so you just need to do that at the tail
Exact same behavior this way
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
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
Okay. Well this is the pattern I have used in my code. I will defer to others.
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))))))I’m upgrading my timeseries rendering chart from v1 to v3 and this conversation was very helpful for that work turns out