This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-09-08
Channels
- # announcements (9)
- # babashka (17)
- # beginners (26)
- # biff (2)
- # calva (5)
- # cider (11)
- # clara (6)
- # clojure (48)
- # clojure-europe (34)
- # clojure-nl (1)
- # clojure-norway (34)
- # clojure-uk (2)
- # clojurescript (22)
- # clr (11)
- # code-reviews (5)
- # conjure (3)
- # datomic (26)
- # emacs (14)
- # fulcro (10)
- # hyperfiddle (70)
- # lsp (34)
- # malli (5)
- # missionary (5)
- # off-topic (34)
- # releases (1)
- # shadow-cljs (19)
- # tree-sitter (1)
- # xtdb (25)
How do I make reusable components like:
(e/defn Card
[something body]
…stuff
(dom/div body))
I don’t quite understand how to delay the evaluation of the body, it always renders first in the dom before “stuff”E.g.,
(defmacro button
[{:keys [kind]
:or {kind :white}
:as props}
& children]
`(dom/button …
~@children))
can you give an example of a component that doesn't behave as you expect it to? (do (dom/div (dom/text "a")) (dom/div (dom/text "b")))
renders in the order a then b
ah, do you mean you call it like (Card. something (dom/div (dom/text "c")))
? The simplest is to thunk it, i.e. send in (e/fn [] (dom/div))
and call it (dom/div (Body.))
Yeah, this is more about composition and reusability as I understand it. Like:
(Container.
(Section.
(Item.)
(Item.)
(Item.))
(Section.
(Thing.)
(Thing.)
(Thing.)))
Where e.g. Section
is some bundle of structure and behaviour that is agnostic to its children.I think this is people looking at it with the reagent glasses on. When writing clojure code, it's obvious to everyone in (section (item) (item) (item))
items will be evaluated before section gets called
It’s not about obviousness, it’s about getting at the pattern of reusable “components”. I understand why Item
is evaluated before Section
.
To be fair, it’s not about Reagent specifically. It goes back to HTML, and virtually every frontend framework mimics that structure. I.e., it’s possible to place an on-screen element inside of another on-screen element, where the second one doesn’t necessarily know or care about what you place inside it.
Electric, of course, is a different model, so it’s obvious why this doesn’t happen. I don’t think it’ll be obvious to every person that tries out Electric, and I do think it might put some constraints on what type of higher order “components” are possible for a layman to write, and therefore might impact what the ecosystem around Electric ends up looking like.
> It goes back to HTML, and virtually every frontend framework mimics that structure. I.e., it’s possible to place an on-screen element inside of another on-screen element, where the second one doesn’t necessarily know or care about what you place inside it.
As I showed this is true for electric as well. (Section. Item Item Item)
sends in 3 components and Section
will call new
on them, not caring about their structure
it's true we're using novelty budget, so users need adapting. But there's nothing stopping one from composing and building higher-order components
Right, but say Item takes arguments. And say it’s not one level, but 3-5 levels of children. It’s possible, I’m just saying that it’s not necessarily obvious how to do it, and it’s not necessarily ergonomic.
I think it’s possible to extrapolate from the above, no? Hang on, I’ll provide another example.
Pseudocode meant to mirror something you might see in React etc.:
(Menu. {:orientation :vertical}
(Section. {:title "Profile"}
(Item. {:title "Edit profile"
:image (Image. (UserImage.))}
(dom/on "click" …))
(Item {:title "Log out"}
(dom/on "click" …)))
(Section. {:title "Documents"}
(for-by [document documents]
(File. {:icon (DocumentIcon.)} document))))
You can get to this with macros or lambdas or a combination thereof. You can also forgo the reusable part and make ProfileSection and DocumentSection, ProfileItem and LogOutItem etc., and not compose them this way.Also, if you believe it to be a non-issue, I won’t press it. For me personally, it’s not a show-stopper.
sure, thanks! There are many ways to structure your example, some brainstormed ideas
• if Menu
is just dom/menu
with some styles you could extract the styles in a separate helper and write (dom/menu (MenuStyles. {:orientation :vertical}) ...)
• if you want to mimic the dom helpers you'd write (demacro menu [styles & body]
(dom/menu (MenuStyles. styles) @body))`
• you can also thunk all the way, although in this example it would get pretty verbose
• the dom/on
examples cannot work as-is, you really need a macro for that. Also, you're injecting behavior, which means the component isn't really a black box, suggesting the abstraction is leaking
• these high-level components tend to turn into god-class-like behemoths, maybe one should just inline them and factor out the commonalities into helper functions
The list resembles typical coding questions - where to put abstractions in and where to leave them out, how to factor our code, do we want to write a macro for syntax sugar etc.
Regarding god-class stuff, the goal is the opposite: rather than having a huge props API to some enormous thing that internally hides/shows components, flips behaviour on it’s internally constructed children, etc. in response to arguments, you try to shrink the API and expose the generic capability of being able to take children from the outside. The constraint being that the parent can’t then inject behaviour on them—this is done from the outside at the moment of composition. There’s no silver bullet, and it usually ends up being a trade-off where the practical solution is some level of props API + semi-generic ability to take some class of children. A menu component can’t take arbitrary children, obviously, only those that make sense in a menu. But it can be agnostic to the children of the children, etc., which a mega-component that controls all levels of children from the top down, internally, can’t.
the unit of reuse is lambda, components are lambda, this appears to be a discussion of syntax , not of reuse
Yeah, it’s mostly about ergonomics and approachability. Stuff from hyperfiddle.electric-dom2
compose declaratively, as does the stuff from ui
, whereas your own “components” do not. This is surprising, hence the original question.
To be absolutely clear, my points are not: • About bashing Electric: it’s fantastic • Saying that Electric’s model is wrong • Making any arguments for how you should prioritize or structure your time It’s only about trying to convey that for some, this is surprising. I then go on to speculate that this is one variable that might impact how easily libraries, third-party components etc. made for Electric proliferates. I think an “at hand” pattern for dealing with it would help. With which you might disagree.
Ah ok specifically the change in apparent semantics between the dom macros and regular Electric code is what is surprising, that i accept as true
Our official stance on this is that we care much more about language semantics than having easy dom authoring syntax this year. Committing to a syntax too soon (before understanding advanced use cases) risks getting it wrong
as for data composition vs lambda composition, it's important to note that lambda composition is more fundamental, data can be implemented in terms of lambda
{:a {:b 2}}
vs (hash-map :a (hash-map :b 2))
you could imagine an OOP pattern like this quick sketch, but it has issues to work out
(e/defn Div [& children]
(e/fn []
(let [el (goog.dom/createElement "div")]
(.appendChild dom/node el)
(binding [dom/node el]
(e/for [Child children] ; dynamic traversal at runtime
(Child.))))))
(Div. (Div.) (Div. (Div.)))
anyway the upcoming differential Electric has implications in how lower level things are done, which is another reason why we choose not to work on syntax until Electric is more complete than it is today
@U02ABMYDCGK the intuition to use here is that in an effectful dom abstraction, dom effects (say dom/text
) evaluate like println
, does that match what you experience?
Yes. That matches what I am seeing and works nicely. I built a nice little UI but I have a lot of repeated code. I am just looking for a way to pass children into a component of my creation. Specifically a Card component with an x in the top right that takes a call-back to reset an atom and arbitrary children that will render inside the card. The effectful nature of electric dom makes that hard (for me!) to do. I don’t want to write macros for components 😝
Also, thanks for building this thing. It is a ton of fun and feels magical.
Do you have your answer? wrap your dom markup in e/fn and pass it around as lambda, call it at the place where you want to splice it in
Ah I understand now what y’all were getting at about lambda as the abstraction. Works for me! Thanks for the help.
One other thing that “got me” was having to call new on the e/fn lambda.
Shower thought: Clojure has clojurians, so it follows that Electric has… Electricians?
ha ha
If I have the following let block
[!a (atom {:k1 (some-fn) :k2 val2})
a (e/watch a)]
and val2
is some dynamic value (e.g. derived from path
, defined with e/def
like in https://github.com/hyperfiddle/electric/blob/master/src/contrib/electric_goog_history.cljc#L63C50-L63C57).
1. what happens when val2
changes (in this case, due to navigating to a new page)?
a. Do we get a new instance of !a
or is the :k2
value of !a
updated?
b. If we get a new instance of !a
, is some-fn
also called again when val2
changes?
2. if we (swap! !a assoc :k2 123)
, and then val2
gets a new value (by navigating to a new page), will (get a :k2)
have that new value or 123
?1a: yes, you get a new value. A clj(s) function is re-run when any of its arguments change. In this case the hash-map is re-built (the hash-map literal compiles down to (hash-map :k1 (some-fn) :k2 val2)
), so atom
re-runs as well
2: yes, you'll see the new value, since the change of val2
builds a new atom which gets a new watch etc
what is the purpose of pushing a reactive value into an atom to later watch and retrieve it again?
I kind of stumbled on this by trying something silly (i rewrote it, because it felt wrong). But since it worked, I started wondering why it works and what happens under the hood. Good chance to get a better intuition for Electric. Thanks for the explanations, all clear!
no problem! In cases where you have to do this it might be simpler to pull the reactive part out
(let [!a (atom {:k1 (some-fn)})]
(swap! !a assoc :k2 val2)
(e/watch !a))
Now the atom is stable and only the swap!
re-runsOh, cool, I was looking for sth like that! What about a slightly different scenario:
(let [!a (atom {:k1 1
:k2 2})
a (e/watch a)
k1 (a :k1)
k2 (a :k2)]
(Component1. k1)
(Component2. k2)
(Component3. a))
If (a :k1)
changes, do both Component1 and Component3 get re-rendered? Or only parts of them which actually use the k1
value (either the k1
let binding directly, which is passed down, or e.g. via (get a :k1)
somewhere downstream)?Based on what you just said, seems like both [k1 (a :k1)]
and [k2 (a :k2)]
would be re-evaluated, because their input changed (I'm assuming (a :k1)
is the same as (get a :k1)
. And then components 1 and 3 would be re-computed, since their arguments changed, but component 2 would not since its input value didn't change?
the update propagates through them and only calls that depend on the value will re-run. It's a DAG
correct, (:k2 a)
will re-run but return the same value, therefore downstream dependencies will use the cache (last run's result)
OK, so the caching is really granular and done at the level of map values as well, rather than at the whole atom level. Really impressive 🔥
2 questions: is there any reason to consider sth like htmx with electric? also, what would it take to use electric for TUI?
htmx is built around RESTful communication. Electric abstracts the network away. I don't see a good fit here personally
re TUI - definitely doable, one would need to build the reactive layer like we built for the DOM, <https://github.com/hyperfiddle/electric/blob/983544cb84819ab1ff3bfefe316192da648bd18a/src/hyperfiddle/electric_dom2.cljc%7Cwhich is <400loc> with some tests and deprecated fns
now that i think about it, not much. declarative ui takes care of most things. but i was considering sth like https://github.com/plumatic/dommy for making handlers in vanilla js less painful to write
are you asking for a nicer electric-dom authoring syntax?
is there a demo of typeahead/tag-picker somewhere? need to make a "start typing, show existing items, if none of those work then make new thing" interface
We have several different impls spread across projects that need consolidation
One that is bundled with electric is this, it's old though – https://electric-demo.fly.dev/(wip.tag-picker!TagPicker)
hmm, getting maximum call stack size exceeded
if I use tag-picker within a (for-by (dom/div ...
this works
(e/for-by
:id [{:keys [id text editing? importance base-importance]}
(new (q '[[id ::node-type :problem]
[id ::node-text text]
[id ::editing? editing?]
[id ::importance importance]
[id ::base-importance base-importance]
#_[id ::blocks blocks]]))]
(let [x (atom nil)]
(ui/tag-picker (e/watch x)
(e/fn [add] nil)
(e/fn [disj] nil)
(e/fn [search-input] nil)
(e/fn [e] nil)))
...
this doesnt
(e/for-by
:id [{:keys [id text editing? importance base-importance]}
(new (q '[[id ::node-type :problem]
[id ::node-text text]
[id ::editing? editing?]
[id ::importance importance]
[id ::base-importance base-importance]
#_[id ::blocks blocks]]))]
(dom/div
(let [x (atom nil)]
(ui/tag-picker (e/watch x)
(e/fn [add] nil)
(e/fn [disj] nil)
(e/fn [search-input] nil)
(e/fn [e] nil)))