Fork me on GitHub
#reagent
<
2021-02-25
>
marek-sed09:02:59

Hi everybody I am working on my first clojure project. reagent is absolute joy to use but I really miss some patterns that I used heavily in vanilla react. Is there a way to manipulate children of components, how to do things you can achieve with

React.Children.map React.cloneElement, React.Children.toArray(children).filter(props => props.cond1 && props.type === typeof(SomeElement))

p-himik11:02:41

I have no idea what this code snippet is supposed to achieve. Can you describe it in plain English?

marek-sed13:02:44

so lets say I want to create api I like this

[tabs {:defaultId :profile}
  [tab {:id :profile}]
  [tab {:id :about]]
tabs component contains atom of which tab is selected and applies correct active class to selected child. Also attaches on-click handlers. in react it would be
function tabs({children}) = {
  const [selectedId, set] = state
  const onClick = (id) => set(id)
 
  return React.Children.map(children, child => React.cloneElement(child, {
     className: child.props.id === selectedId ? "active" : null,
      onClick: onClick
   }
}

marek-sed13:02:25

another idea would be depending of parent state diplay filter the children collection

function tabs({children, disabledIds}) = {
  const [selectedId, set] = state
  
  const onClick = (id) => set(id)
  return React.Children.map(children, child => { 
         if (child.type === typeof(Tab) && disabledIds.includes(child.props.id)) { 
            return React.cloneElement(child, {
                     className: child.props.id === selectedId ? "active" : null,
                     onClick: onClick
             })
          } 
          return null
   }
}

p-himik13:02:53

My take would be to not write components that way at all. It's fragile and hard to extend. tabs either has to be a controlled component (you pass it the state if it needs it, and you pass it to each tab as well) or it has to receive a specification of tabs and not the tabs themselves. Or a combination of both. Technically, you can use the [tab {:id ...}] hiccup as a specification, but that's still fragile and hard to extend. If you want some inspiration, take a look at how re-com tackles such things.

p-himik13:02:50

To rephrase my take - don't modify children in parents, if you can help it. Always try to construct the right Hiccup in the first place.

marek-sed13:02:17

I get that, but this is actually used to avoid huge config maps in your component api design, and essentialy just write dom. lets think about button api. I would prefer to use this button, which sets the correct margin left or right for icon. Dependeing if it is present in dom or not.

[button {:variant :primary} [icon {:icon-name :add}] "Click me!"] 
[button {:variant :primary} "Click me!" [icon {:icon-name :add}]] 
then this
[button {:variant :primary :icon-name :add :iconLeft true] "Click me!"]

marek-sed13:02:13

and I don’t want to force it, I am just constantly thinking about api design of components in this manner. Because I am use to it. It might be the case that this is incorrect mindset and reagent doesnt support it.

p-himik13:02:49

> button sets the correct margin Oh no, no-no-no. Use CSS or CSS-in-JS or whatever. What you're trying to solve is not a new problem. > avoid huge config maps I'm not sure what huge maps you're talking about. For example, here's how your tabs component would be used if I were the one to implement it:

[tabs {:tabs [{:id :tab-1, :label "Tab 1"},
              {:id :tab-2, :label "Tab 2"}]}]
I don't see any huge maps. :) If you really don't like seeing that :tabs key in there, just inline the tabs declarations as children:
[tabs
  {:id ...}
  {:id ...}]

marek-sed13:02:41

ok this a very good counter example

p-himik13:02:01

Definitely take a look at re-com's implementation.

marek-sed13:02:25

Can I have one more question?

p-himik13:02:05

Of course.

marek-sed13:02:28

was thinking of another example but it doesn’t seem so bad in hiccup then in jsx. Will require some mindset update.

marek-sed13:02:43

will check the re-com thanks for the time 👍

👍 3
marek-sed14:02:27

Found a good re-com example, lets make a map “huge”

(defn tooltips-demo
  []
  (let [tab-defs        [{:id ::1 :label "Left Tab"   :tooltip "This is the first tooltip..."}
                         {:id ::2 :label "Middle Tab" :tooltip "This is the second tooltip..."}
                         {:id ::3 :label "Right Tab"  :tooltip "This is the third and the final tooltip!"}]
        selected-tab-id (reagent/atom (:id (first tab-defs)))]
    (fn []
      [v-box
       :gap      "20px"
       :children [[p "Hover over a tab to see a tooltip."]
                  [horizontal-bar-tabs
                   :model     selected-tab-id
                   :tabs      tab-defs
                   :on-change #(reset! selected-tab-id %)]]])))
Lets say I receive requirments to show tooltip with different style, text color depending on some state so I add :tooltipStyle. Lets say I need to have tabs on the bottom. adding another key into map :tooltipPosition :bottom , tabs can also have an icon on left or right, so we add :icon-name :icon-position :icon-style. Tabs can have specific style, underlined, bold, etc :tab-style So the reagent way, is to extend a tab-defs by these new properties. Which for last couple of years I always tried to avoid in jsx. It was easier to see whats going how are things composed. And I mean that more to explain were I am coming from, then complaining that I can’t do that.

marek-sed14:02:01

thinking out loud so I would end up with (omitting values)

{:id ::1 :tab {:label {:content {:text :icon :icon-position} :tooltip {:position :style}}
so probably the motivation to avoid this in JSX land, is that xml and jsons are so foreign to each other. while in clojure this seems very natural.

p-himik14:02:53

Everything that you describe can be handled by just passing the correct hiccup to :label. Not sure what you mean by "having tabs on the bottom" when you provide tooltip position as an example.

p-himik14:02:28

A tabs component should manage tabs. That's it. It shouldn't have its API be designed around separate tabs, especially not about how each and every tab should look.

p-himik14:02:10

A tabs component is like a box with slots. It doesn't decide what exactly goes into which slot. But it decides where each slot is. Something like that.

marek-sed14:02:01

yeah I think I understand the regeant way now 🙂

marek-sed14:02:42

thx once again 🙂

p-himik15:02:54

Sure thing.

Old account12:02:19

Hello, how to make sense of [:input {:type "checkbox" :on-change #(js/console.log %)}] :on-change argument? I just want to find out if it is selected or not...

vanelsas12:02:45

Checkbox has a field called :checked that you can use to check/uncheck the checkbox. You could try something like this (haven't verified if it works).

✔️ 3
kozmicluis17:02:49

same way you'd make sense of it in javascript, using ".target.checked"

kozmicluis17:02:39

[:input {:type "checkbox" :on-change #(-> % .-target .-checked js/console.log)}]

kozmicluis17:02:45

or you could bind the property to state, as vanelsas said.

kozmicluis17:02:30

as he suggests, since the checkbox's initial state is "non checked", you can safely assume most of the time that the first on-change event means the checkbox changed to "checked", and so forth.