reagent

A minimalistic ClojureScript interface to React.js http://reagent-project.github.io/
Adriaan Callaerts 2020-11-26T07:56:19.254200Z

Hi all. I'm at a loss of how to properly make a higher-order component work. I want to have a component that wraps others, adding/overriding props on its children. I notice that the rendered output still uses the props from the original child, which hints to me that reagent is rendering bottom-up (child first, parent later). However, for my purposes I would like to get a chance for the parent to augment the props of its child before it gets rendered. How should I go about that? (I can provide code snippets if my explanation isn't clear enough)

p-himik 2020-11-26T08:46:03.254300Z

Your description is very vague - indeed, the code is required.

Adriaan Callaerts 2020-11-26T08:47:54.254500Z

(defn- bind-component [component]
  [:> (oget form-context "Consumer") {}
   (fn [context-namespace]
     (reagent/as-element
      [:> (oget form-attribute-context "Consumer") {}
       (fn [context-attribute]
         (reagent/as-element
          [component {:namespace (context-value->keyword context-namespace)
                      :attribute (context-value->keyword context-attribute)}]))]))])

(defn label [props & children]
  (bind-component (fn [{:keys [namespace attribute]}]
                (into [:label (merge props
                                     {:for (namespaced-attribute->id namespace attribute)})]
                      children))))

(defn input-binding [{namespace-from-props :namespace attribute-from-props :attribute} & children]
  (bind-component (fn [{namespace-from-context :namespace attribute-from-context :attribute}]
                    (let [namespace (first (filter some? [namespace-from-props namespace-from-context]))
                          attribute (first (filter some? [attribute-from-props attribute-from-context]))
                          mapped-children (map-indexed
                                           (fn [idx [type props & children]]
                                             (println "mapping child of input-binding" type props)
                                             (into [type
                                                    (merge props
                                                           {:id (str (namespaced-attribute->id namespace attribute) (when (not= idx 0) (str "." idx)))
                                                            :value @(subscribe [::subs/attribute-value namespace attribute])
                                                            :on-change (fn on-bound-input-change [value & _anything]
                                                                         (println "synchronously dispatching change-atrribute-value of bound input" namespace attribute value)
                                                                         (dispatch-sync [::events/change-attribute-value namespace attribute value])
                                                                         (when (fn? (:on-change props))
                                                                           (println "calling custom on-change")
                                                                           ((:on-change props) value)))
                                                            :on-blur (fn [e] (dispatch [::events/implicitly-confirm-attribute-value namespace attribute (oget e "target.?value")]))})]
                                                   children))
                                           children)]
                      (println "mapped children:" mapped-children)
                      (into [:div {:title (namespaced-attribute->id namespace attribute)}]
                            mapped-children)))))

;; and then in some other component:

(defn other-component [search]
  [input-binding nil
       [date-picker {:on-change search}]])

Adriaan Callaerts 2020-11-26T08:50:47.254800Z

I realize all the namespace-stuff is obfuscating the problem, but since I'm afraid it might be related to the problem I decided to leave it in...

p-himik 2020-11-26T09:13:34.255Z

Too much is going on in your code for me to efficiently find the problem. E.g. have you tried reproducing it without that strange bind-component? Also, just in case - (first (filter some? [a b])) can be replaced with just (or a b), unless a can be false.

Adriaan Callaerts 2020-11-26T09:14:40.255200Z

I'll come up with a smaller reproduction πŸ˜‰ Might take few minutes though...

p-himik 2020-11-26T09:15:04.255400Z

But I'll say this in advance - your initial assumption that you can just "patch" children is correct. You can absolutely do that, assuming that it's the parent component that gets re-rendered. If you manage to change the "patching" process in such a way that doesn't actually re-render the parent component, then it will not work.

Adriaan Callaerts 2020-11-26T09:16:07.255700Z

do I need to do anything in particular to make the parent re-render? Or is it sufficient to ensure that the new children are not identical to the originals?

Adriaan Callaerts 2020-11-26T09:16:40.255900Z

I have a background in react and cljs/reagent is newer to me, so I'm sometimes unsure which of my knowledge translates over πŸ˜‰

p-himik 2020-11-26T09:30:11.256100Z

Reagent is just a wrapper over React with some ratom magic. Since you're not using ratoms above, there's no magic. Imagine you get a child React element in a component and make that component create a new element based on it. Same thing.

p-himik 2020-11-26T09:30:32.256300Z

Oh wait, you're using ratoms, my bad.

p-himik 2020-11-26T09:31:09.256500Z

Still, any change to that sub should re-render the parent.

p-himik 2020-11-26T09:37:23.256700Z

As one would expect, works just fine:

(ns clj-playground.core
  (:require [reagent.dom]
            [clojure.browser.dom :as dom]))

(defn parent [{:keys [extra-value]} & children]
  (into [:div]
        (map (fn [[child-component child-props & grandchildren]]
               (into [child-component (assoc child-props :extra-value extra-value)] grandchildren)))
        children))

(defn child [{:keys [value extra-value]} & children]
  (into [:div
         [:span "Value: " value]
         (when extra-value
           [:<>
            [:br]
            [:span "Extra value: " extra-value]])]
        children))

(defn app []
  [parent {:extra-value "there"}
   [child {:value "Hello"}]
   [child {:value "You"}]])

(defn ^:export init []
  (reagent.dom/render [app] (dom/get-element "app")))

Adriaan Callaerts 2020-11-26T09:39:05.256900Z

Ok, thanks for assuring me that the rpincipal idea should work. Now I'll go back and dig into why my complicated specific case is not behaving πŸ˜›

1πŸ‘
p-himik 2020-11-26T08:46:03.254300Z

Your description is very vague - indeed, the code is required.

Adriaan Callaerts 2020-11-26T08:47:54.254500Z

(defn- bind-component [component]
  [:> (oget form-context "Consumer") {}
   (fn [context-namespace]
     (reagent/as-element
      [:> (oget form-attribute-context "Consumer") {}
       (fn [context-attribute]
         (reagent/as-element
          [component {:namespace (context-value->keyword context-namespace)
                      :attribute (context-value->keyword context-attribute)}]))]))])

(defn label [props & children]
  (bind-component (fn [{:keys [namespace attribute]}]
                (into [:label (merge props
                                     {:for (namespaced-attribute->id namespace attribute)})]
                      children))))

(defn input-binding [{namespace-from-props :namespace attribute-from-props :attribute} & children]
  (bind-component (fn [{namespace-from-context :namespace attribute-from-context :attribute}]
                    (let [namespace (first (filter some? [namespace-from-props namespace-from-context]))
                          attribute (first (filter some? [attribute-from-props attribute-from-context]))
                          mapped-children (map-indexed
                                           (fn [idx [type props & children]]
                                             (println "mapping child of input-binding" type props)
                                             (into [type
                                                    (merge props
                                                           {:id (str (namespaced-attribute->id namespace attribute) (when (not= idx 0) (str "." idx)))
                                                            :value @(subscribe [::subs/attribute-value namespace attribute])
                                                            :on-change (fn on-bound-input-change [value & _anything]
                                                                         (println "synchronously dispatching change-atrribute-value of bound input" namespace attribute value)
                                                                         (dispatch-sync [::events/change-attribute-value namespace attribute value])
                                                                         (when (fn? (:on-change props))
                                                                           (println "calling custom on-change")
                                                                           ((:on-change props) value)))
                                                            :on-blur (fn [e] (dispatch [::events/implicitly-confirm-attribute-value namespace attribute (oget e "target.?value")]))})]
                                                   children))
                                           children)]
                      (println "mapped children:" mapped-children)
                      (into [:div {:title (namespaced-attribute->id namespace attribute)}]
                            mapped-children)))))

;; and then in some other component:

(defn other-component [search]
  [input-binding nil
       [date-picker {:on-change search}]])

Adriaan Callaerts 2020-11-26T08:50:47.254800Z

I realize all the namespace-stuff is obfuscating the problem, but since I'm afraid it might be related to the problem I decided to leave it in...

p-himik 2020-11-26T09:13:34.255Z

Too much is going on in your code for me to efficiently find the problem. E.g. have you tried reproducing it without that strange bind-component? Also, just in case - (first (filter some? [a b])) can be replaced with just (or a b), unless a can be false.

Adriaan Callaerts 2020-11-26T09:14:40.255200Z

I'll come up with a smaller reproduction πŸ˜‰ Might take few minutes though...

p-himik 2020-11-26T09:15:04.255400Z

But I'll say this in advance - your initial assumption that you can just "patch" children is correct. You can absolutely do that, assuming that it's the parent component that gets re-rendered. If you manage to change the "patching" process in such a way that doesn't actually re-render the parent component, then it will not work.

Adriaan Callaerts 2020-11-26T09:16:07.255700Z

do I need to do anything in particular to make the parent re-render? Or is it sufficient to ensure that the new children are not identical to the originals?

Adriaan Callaerts 2020-11-26T09:16:40.255900Z

I have a background in react and cljs/reagent is newer to me, so I'm sometimes unsure which of my knowledge translates over πŸ˜‰

p-himik 2020-11-26T09:30:11.256100Z

Reagent is just a wrapper over React with some ratom magic. Since you're not using ratoms above, there's no magic. Imagine you get a child React element in a component and make that component create a new element based on it. Same thing.

p-himik 2020-11-26T09:30:32.256300Z

Oh wait, you're using ratoms, my bad.

p-himik 2020-11-26T09:31:09.256500Z

Still, any change to that sub should re-render the parent.

p-himik 2020-11-26T09:37:23.256700Z

As one would expect, works just fine:

(ns clj-playground.core
  (:require [reagent.dom]
            [clojure.browser.dom :as dom]))

(defn parent [{:keys [extra-value]} & children]
  (into [:div]
        (map (fn [[child-component child-props & grandchildren]]
               (into [child-component (assoc child-props :extra-value extra-value)] grandchildren)))
        children))

(defn child [{:keys [value extra-value]} & children]
  (into [:div
         [:span "Value: " value]
         (when extra-value
           [:<>
            [:br]
            [:span "Extra value: " extra-value]])]
        children))

(defn app []
  [parent {:extra-value "there"}
   [child {:value "Hello"}]
   [child {:value "You"}]])

(defn ^:export init []
  (reagent.dom/render [app] (dom/get-element "app")))

Adriaan Callaerts 2020-11-26T09:39:05.256900Z

Ok, thanks for assuring me that the rpincipal idea should work. Now I'll go back and dig into why my complicated specific case is not behaving πŸ˜›

1πŸ‘
reefersleep 2020-11-26T12:06:36.257900Z

What is the point of reagent.core/rswap!? I don’t quite understand the docstring, and I don’t get when you’d want to use it over swap!.

p-himik 2020-11-26T13:27:36.258Z

I suppose regular swap! has some issues with the code like this: (swap! a (fn [v] (swap! a 1) (inc v))).

p-himik 2020-11-26T13:27:59.258200Z

No idea what the use cases might be.

reefersleep 2020-11-26T14:57:35.258400Z

I see. And I agree. πŸ™‚ Thanks! @p-himik