Fork me on GitHub
#clojurescript
<
2023-01-19
>
kennytilton14:01:29

Is there any way to use ClojureScript for SVG event handlers? JS/SVG seem to be a bit fussy about how we specify those. Very good discussion here: https://stackoverflow.com/questions/16472224/add-onclick-event-to-svg-element Excerpt: "It appears that all of the JavaScript must be included inside of the SVG for it to run. I was unable to reference any external function, or libraries. This meant that your code was breaking at svgstyle = svgobj.getStyle();" This example avoids the getStyle accessor.

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "">
<svg xmlns='' version='1.1' height='600' width='820'>
  <script type="text/ecmascript"><![CDATA[
      function changerect(evt) {
        var svgobj=evt.target;
        svgobj.style.opacity= 0.3;
        svgobj.setAttribute ('x', 300);
      }
    ]]>
  </script>
  <rect onclick='changerect(evt)' style='fill:blue;opacity:1' x='10' y='30' width='100'height='100' />
</svg>
from https://stackoverflow.com/questions/16472224/add-onclick-event-to-svg-element Even if CLJS could generate that (can it? I could not see a way), mxWeb relies for its reactive fun on kicking off non-JS propagation functions. btw: SVG handlers can be text scripts, eg :onclick "alert('hi mm')", but the strings are still constrained to pure JS AFAICT. Are we doomed to pure JS in SVG?

magnars15:01:15

Do you need to serve the svg in .svg -files? We generally build our svg with cljs.

[:div "Here's some svg:" [:svg [:path {:d "..."}]]]

p-himik15:01:35

@U0PUGPSFR Have you seen the comment under the accepted answer there?

p-himik15:01:25

Also, other answers suggest that it's possible to refer to any code within a JS event handler attached to an SVG element if you attach that handler dynamically.

kennytilton15:01:34

@U07FCNURX Thx. Does Hiccup support event handlers calling arbitrary JS? FWIW, mxWeb does build SVG elements using createElementNS, and offers its own alternative to Hiccup:

(defn radial-gradient []
  ;; 
  (svg {:width 120 :height 240}
    (defs
      (radialGradient {:id :RG1}
        (stop {:offset "0%" :stop-color :red})
        (stop {:offset "100%" :stop-color :blue})))
    (rect {:x 10 :y 10 :rx 15 :ry 15 :width 100 :height 100
           :fill "url(#RG1"
           :onclick "alert('Hi, Mom!')"})))
So I can get down and dirty with SVG, just gotta find the magic incantation if any exist. Staring now at the comments @U2FRKM4TW helpfully pointed out.

p-himik15:01:57

Hiccup and JS are completely orthogonal. Reagent can imperatively attach handlers to a generated SVG element. Other popular UI libraries can probably do it too because something like this is a very common requirement when working with JS UI libraries.

thheller15:01:08

in regular DOM you add an event listener via (.addEventListener that-svg-node "click" (fn [e] ...)). how you obtain that-svg-node and WHEN you do this differ depending on what you are using to build the DOM. If you are creating it anways via createElementNS you can add it and just handle :onclick accordingly. just a CLJS fn should be fine. a string is madness if you want code

kennytilton17:01:06

Agreed on the string madness, @U05224H0W. 🙂 But, no, the CLJS function as a handler does not fly. This works:

(defn radial-gradient []
  ;; 
  (svg {:width 120 :height 240}
    (defs
      (radialGradient {:id :RG1}
        (stop {:offset "0%" :stop-color :red})
        (stop {:offset "100%" :stop-color :blue})))
    (rect {:x 10 :y 10 :rx 15 :ry 15 :width 100 :height 100
           :fill "url(#RG1"
           :onclick "console.log('Hi, Mom!')"})))
But swap in (fn [evt] (prn "hi mom')) and it works in the sense that it complains the function is un-named! Make that (fn foo [evt] (prn "hi mom"))) and there is no error, but nothing happens. Encouraged by @U2FRKM4TW I tried the geometry example in Reagent and that works (yay!) via some typical ReactJS subterfuge in which the handler does not really go on the SVG DOM element, but we code it that way and the framework arranges for a handler on the window to do the right thing. Fortunately, mxWeb talks straight to the DOM via proxy SVG CLJS elements, so I just have to do sth similar. Thx all!

👍 2
skylize20:01:07

> and the framework arranges for a handler on the window to do the right thing To the the extent that it can be practical, a single delegating event handler on window is way better for users anyway, if you have more than a very small handful of possible events to listen to.

thheller20:01:13

why does it not fly? I don't know how you handle :onclick? does it just (.setAttribute the-el "onclick" ...) then yes of course that won't work

kennytilton20:01:32

@theller: "not fly" as described: it sees I am providing a function (good!) and complains if it is not a named function, then when I provide a name it silently ignores the function. The SO story OP reported the same: no error, just ignored. But indeed I am using setAttribute. I will see if setAttributeNS works better. The MDN doc expresses flexibility, but may as well use the write call.

thheller21:01:49

can you point me to the code trying to set this?

thheller21:01:57

DO NOT use setAttributeNS either

thheller21:01:09

setAttribute takes 2 string args. it cannot possible take a direct function reference

thheller21:01:55

you either (set! the-dom-element -onclick that-fn) or (.addEventListener the-dom-element "click" that-fn). setAttribute cannot be used if you want to work with actual code values

👍 2
thheller21:01:24

ignore that SO question. it is talking about svg constructed by the server and sent as raw svg. not svg constructed by client side JS, that is an entirely different story

thheller21:01:05

unless you are running that code server side of course. can't tell from what little you shared here, but guessing from you mentioning createElementNS I though client side

kennytilton21:01:29

No server involved other than figwheel's. Here's the error if I provide an SVG rect with an onclick CLJS fn:

Uncaught SyntaxError: Function statements require a function name (at (index):1:1)
From:
(rect {:x            10 :y 10 :width 30 :height 30
             :onclick (fn [e] (prn :hi-mom)) ;;"alert('hi mom')"
             :stroke-width 5 :fill :transparent})
It means a lot to me that the browser is seeing a function and whining about the name, and that providing the name avoids the error but we see no behavior on clicking. And that a string onclick works fine. Otherwise, I would worry about mxWeb confusing things by supplying the onclick to the browser brokenly. btw, no better with setAttributeNS.

kennytilton22:01:58

btw, this question does indeed show an <svg> element with a child <rect>:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "">
<svg xmlns='' version='1.1' height='600' width='820'>

  <script type="text/ecmascript"><![CDATA[
      function changerect(evt) {
        var svgobj=evt.target;
        svgobj.style.opacity= 0.3;
        svgobj.setAttribute ('x', 300);
      }
    ]]>
  </script>

  <rect onclick='changerect(evt)' style='fill:blue;opacity:1' x='10' y='30' width='100'height='100' />
</svg>
Can CLJs do that <script>? Might still be too limited for useful interop, however.

kennytilton00:01:40

@U90R0EPHA wrote: "To the the extent that it can be practical, a single delegating event handler on window is way better for users anyway, if you have more than a very small handful of possible events to listen to." Better for users?! 🙂 Talk to me. How so? Btw, as a rule, when creating a library to wrap X, do not break X. That is how we got IE. 🤯

kennytilton09:01:20

Ah, @U05224H0W was talking about the suggestions in the comments. Right, I just drilled down into the SVG itself. Crazy stuff.

kennytilton09:01:20

Thx, @theller! I went with addEventListener. Too easy! 🙂 🙏

kennytilton10:01:22

The code, FWIW:

(defn svg-dom-create [me dbg]
  (let [svg (.createElementNS js/document ""
              (mget me :tag))]
    (rmap-meta-setf [:dom-x me] svg)
    (.setAttributeNS svg
      ""
      "xmlns:xlink"
      "")
    (doseq [ak (:attr-keys @me)
            :let [ak$ (name ak)
                  av (ak @me)]]
      (if (fn? av)
          (.addEventListener svg
            (if (= "on" (subs ak$ 0 2))
              (subs ak$ 2) ak$)
            (ak @me))
        (.setAttributeNS svg nil ak$ (attr-val$ (ak @me)))))
    (doseq [kid (mget me :kids)]
      (.appendChild svg (svg-dom-create kid dbg)))
    svg))

skylize12:01:46

> Better for users?!  ​ Talk to me. How so? > The process of event listening is fairly heavy to begin with. But we have all learned pretty well by now to avoid touching the DOM whenever we can (because "The DOM is ssslllooooooooooowwww"), and adding/removing an event listener is one of the heaviest updates we can make to the DOM. Here is the first perf test example I came across as demonstration: https://web.archive.org/web/20170121035049/http://jsperf.com/click-perf Made all the way back in early 2015, when jQuery was still quite commonly perceived as useful. Eight years later, using the most expensive smartphone on the market, my browser still shows that adding only (roughly counted) around 160 new listeners to the DOM drags the main thread's processing speed down from 200k-300k ops/sec to around 4.5k ops/sec. That only even measures time spent creating the listeners, with no mention of cpu wasted later searching through them all after each possibly-triggering event, nor the equivalent breakdown time if you need to remove associated elements from the DOM, nor the risk of leaking memory from any listener you forget to remove appropriately. Lots of listeners makes your app overly susceptible to jank. Even when/if a user's device has sufficient raw power to hide that jankiness, reducing of a bunch of listeners down to a few delegators easily shows a meaningful reduction in electricity burning. This makes zero difference in a toy app with... say 10-20 listeners (as long as you don't leave them listening to missing elements). And you can probably get well into the 100s now without really feeling the pain. But when the elements on the page starts ballooning into the 1000s and above, with most of those elements having at least 1 listener, you have extremely high risk of crippled performance across the board, even ignoring all the many much-less-obvious opportunities out there to accidentally hang the browser. If you were to ignore encapsulation, one listener on window or document or #app could easily handle all your events, with no need to ever touch the DOM for updating that listener. (Encapsulation is where the question of practicality comes into play.) Often you can come up with rules that self-correct based on what does/does-not exist in the DOM, in which case you would never need to update the global listener either.

thheller14:01:58

I'm sorry but this is just nonsense. "the DOM is slow" is a terrible meme. The DOM is not slow in general. Handling it incorrectly makes things slow. Also this jsperf event handler comparison is a joke. Attaching one root handler can be good and can be terrible, generalizing it as always better is just nonsense.

kennytilton15:01:13

If we look at that perf test, @U90R0EPHA, we see that the handler is function() {}. My first concern is that a real app will do something in a handler, or why I have it? If that sth takes 200ms, our app will feel quite zippy, but now we are running at 5 ops/sec. In either case: the slowest "many" version ran at 10k ops/sec, adding 0.1 ms per op overhead. Now go back to the "winning" parent case. When we fill in that event handler, we need to add extra processing time to figure out to which of the hundreds of DIVs to pass to the event handler. Prolly just a hash lookup, but not zero. That's what I see, anyway. But thx for your concern!

skylize16:01:52

No, delegation is not free. However, it should be cheap, and you have control of just how cheap (as you point out, likely a hash lookup). Interacting with the DOM is consistently expensive, and event listeners on DOM elements interact with the DOM at multiple levels. That's not to say you necessarily want to over-optimize too early. But it can be a reliable way to get notable wins on CPU time, even without in-depth perf testing; because all you need to know is "we're using a lot of event listeners", which is expensive.

thheller17:01:07

> Interacting with the DOM is consistently expensive

thheller17:01:13

you keep saying this based on what evidence? what exactly is slow? back in the day frameworks like jquery and others did some obviously bad stuff, regular careful DOM interop is fine and fast

thheller17:01:51

needlessly mutating the DOM and triggering paints is of course slow, but touching the DOM and doing needed operations is fine

skylize17:01:09

I am not trying to claim the DOM must be avoided at all costs. But we wouldn't have "Virtual DOM"s if DOM manipulation wasn't expensive. I am just describing a place where in it often can be easy-ish to dramatically reduce especially-expensive DOM ops that are not strictly needed because the work can be delegated through a single listener. > That's not to say you necessarily want to over-optimize too early...

thheller17:01:06

just food for thought though: vanillajs with direct DOM interop and no abstraction is consistently the https://krausest.github.io/js-framework-benchmark/current.html and cannot really be beaten other than not using the DOM at all. so virtual DOM is the expensive slow stuff, the more abstractions you add the slower things get

thheller17:01:05

but yes, reducing the amount of DOM ops to the minimum should be the goal always. don't touch the DOM if you don't need to.

kennytilton17:01:44

"But we wouldn't have "Virtual DOM"s if DOM manipulation wasn't expensive." Actually, we all know 🙂 that virtual DOM is needlessly expensive. It gets generated on every event, then diffed. There are better ways of minimizing DOM. mxWeb, like MobX and binding.scala, intelligently track state dependencies. If a widget needs to change color, that is the only DOM operation done. But Facebook engineers went for VDOM, as have others.

thheller17:01:04

but this meme that it is slow is really not helping. be specific. event handlers have not been a problem in my 20+ years of developing JS stuff

thheller17:01:14

I also don't have millions of them 😉

kennytilton17:01:09

SolidJS is another candidate built around "fine-grained" reactivity: https://www.solidjs.com/ "Components execute just once, when they are first rendered, and Hooks and bindings only execute when their dependencies update. This completely different implementation forgoes a Virtual DOM."

skylize18:01:41

Agreed. Virtual DOMs are unnecessarily expensive. But DOM manipulation is so much more expensive that non-trivial apps can eat the difference and gain tremendous benefit. I am a fan of the approach used by https://github.com/WebReflection/hyperHTML (same basic approach used by lit-html). I spotted that Shadow has https://clojureverse.org/t/modern-js-with-cljs-class-and-template-literals/7450 for tagged template literals. Interested to see if I can get that working nicely in CLJS.

kennytilton18:01:59

@U90R0EPHA What do you think of the point that smart enough web libraries can surgically update exactly and only the DOM elements needed to effect a desired change? As I said, MobX, binding.scala, SolidJS, mxWeb....

skylize18:01:29

Not sure what you are referencing in regards to MobX. From my understanding, that provides state management, closer to Redux in scope than to React. I don't see what that has to do with a comparison against a vdom. (That is what we are comparing here isn't it?) No idea what mxWeb is, and can't seem to easily find reference to it through search engines. As for the others, it's not really clear to me what they are doing. I don't think I have enough info to offer meaningful insight at the moment.

kennytilton20:01:24

@U90R0EPHA wrote: "I don't see what [state management] has to do with a comparison against a vdom. " The view exists to display state, right? eg, If the app decides a field has been entered incorrectly by its user, it might want to change the border from black to red. With React, the entire interface must be re-rendered to VDOM, then all of that VDOM diffed against the prior VDOM, all to determine one DOM element needs it's style attribute changed. Ewww! :) The UI libraries I mentioned, thanks to fine-grained dependency tracking, know that only the style attribute of one DOM element was computed using the user-error property of that field. So these libraries dispatch only that one computation. Then, if and only if the computation produces a different result, they do one setAttribute against the DOM. So that is how state management makes DOM manipulation optimal, and the finer-grained the better. Most reactive tools operate at a grosser level, re-rendering an entire component just to change the border color. That is better than an entire page refresh, of course, but not optimal. And this is not just about performance. Why did that border turn red? Look at the few lines of code that compute the style of that widget. Need to change the behavior? Go hack the same few lines. Granularity equals maintainability. mxWeb is a couple months away from official release, not surprised it did not come up on The Google. https://github.com/kennytilton/matrix/tree/main/cljc/mxweb Or if Flutter universality and performance is appealing, Flutter/MX: https://github.com/kennytilton/flutter-mx

skylize21:01:21

> The view exists to display state, right? > What I mean is that MobX on its own (unless I largely misunderstand what it provides) is largely orthogonal to the goals of a vDOM. MobX helps you manage application state unidirectionally, giving you a description of the current state. Then React takes a generated complete state and diffs that into a render. Certainly there is a relationship between app state and DOM updates, but I don't see how MobX would meaningfuly replace use of a vDOM (which seems to be what you imply it can). Yes. Theoretically, finer grained control should be superior. Your project looks interesting, and I hope it works out well. Interestingly, hyperhtml and lithtml already typically show improved granularity over a vDOM. I doubt it holds a candle against what you are describing. > Each Matrix node/model will transparently become a DOM node, long-lived across many renderings, coming and going only as the display responds to user interaction. > I've had an idea floating around in my head for a while that pointers to all the DOM nodes (including ones that are not currently in the DOM) should be stuffed into the state management system, so they can take part in immutable state updates. Does this have similarity to such an idea?

kennytilton22:01:41

"Then React takes a generated complete state and diffs that into a render." No, when React sees a state change it has everybody regenerate their VDOM, and diffs the new VDOM against the prior VDOM. Here is what looks like a quality discussion around SolidJS, from late 2021: https://news.ycombinator.com/item?id=29388994 I think you will be surprised by how badly VDOM gets beat up. 🙂 The important thing to look for: VDOM was never faster than hand-crafted DOM manipulation, because the latter is narrowly targeted to the minimum set of changes needed. If you are feeling deja vu, this is the HLL vs assembler debate all over again. But with fine-grained state management, a UI library "hand-craft" the DOM automatically, so we still build apps fast. And just like optimizing compilers, an automatic UI updater can actually out-do a manual crafting, because it sees better what does not need to be updated, or conversely avoids bugs by catching everything that needs updating. "but I don't see how MobX would meaningfuly replace use of a vDOM (which seems to be what you imply it can)." Well, there was MobX original, then MobX with React. Then there was MobX State Tree (MST), a conversion from granular in-place state management to the Flux model. So the win is not as clear any more. But in the beginning, MobX tracked dependencies between individual properties of specific instances, so it always new precisely which DOM attributes of which DOM elements needed updating. Consider why FB engineers came up with VDOM. First they went for the excellent view=f(state) thing, and produced a huge win: declarative views that magically kept up with run-time state change. The magic was the problem. How do we know what to update? No problem! We will re-render everything!! But that will be slow! Hmmm...what if we render to an intermediate form, VDOM, then diff! That will let us minimize actual slow DOM updates! Let's ship! Getting back to your question, this is why granular state dependency tracking makes VDOM unnecessary. When state changes, we know everything that requires re-rendering. We're done. Go render surgically that which needs rendering. "all the DOM nodes (including ones that are not currently in the DOM) should be stuffed into the state management system, so they can take part in immutable state updates." I suspect that is what React is doing, in effect. React keeps track of which view function generated which DOM, so the DOM is effectively the terminus of the state DAG, your idea. But React uses VDOM as the implementation bridge between "render everything!" to "update just a little DOM", so that leg of the state propagation is the mechanical diff/update.