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))
I have no idea what this code snippet is supposed to achieve. Can you describe it in plain English?
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...
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).
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
}
}
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
}
}
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.
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.
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!"]
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.
> 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 ...}]
ok this a very good counter example
Definitely take a look at re-com's implementation.
will do
Can I have one more question?
Of course.
was thinking of another example but it doesn’t seem so bad in hiccup then in jsx. Will require some mindset update.
will check the re-com thanks for the time 👍
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.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.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.
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.
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.
yeah I think I understand the regeant way now 🙂
thx once again 🙂
Sure thing.
same way you'd make sense of it in javascript, using ".target.checked"
[:input {:type "checkbox" :on-change #(-> % .-target .-checked js/console.log)}]
or you could bind the property to state, as vanelsas said.
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.