Passing data between reagent components - clojurescript

I have the following reagent components
(defn comments-component [arg-id]
[:p arg-id])
(defn arguments-component [list-of-args]
[:ul.arguments
(for [{:keys [comment upvotes id]} #list-of-args]
^{:key id}
[:li
[:p comment]
[:p upvotes]
[:p id
[modal-button :argument
;title
"Comments"
;body
[:div
[:p "Comments"]
[comments-component id]]
;footer
[:p "OpinionNeeded"] ]]])])
(defn debate []
(let [debate-topic #(rf/subscribe [:debate/topic])
debate-affirmatives (rf/subscribe [:debate/affirmatives])
debate-negatives (rf/subscribe [:debate/negatives])]
(fn []
[:div
[:a {:href "/"} "Return home"]
[:p "This page is being worked on"]
[:div
[:h2 (get debate-topic :title)]
[:p (get debate-topic :description)]]
[:div.columns
[:div.column.is-half
[:p "Agree"]
[arguments-component debate-affirmatives]]
[:div.column.is-half
[:p "Disagree"]
[arguments-component debate-negatives]]]])))
The problem I'm encountering is that the modal button is supposed to create a modal popup with the id for each specific argument, (which I can then use to fetch the comments for that specific argument.)
But instead, I'm getting this bug whereby all the modals for different arguments show the same id.
I cannot figure out why this is happening, but it seems that all the modal-button functions are getting called with the id of the last or first argument to be rendered.
This is what the relevant portion of app-db looks like on this page
:affirmatives [{:id 1, :comment "", :upvotes 0, :topic_id 2, :affirm true} {...}]
:negatives [{:id 2, :comment "", :upvotes 0, :topic_id 2, :affirm true} {...}]
:comments [{:id 1, :comment "", :upvotes 0, :argument_id 1, :topic_id 2} {...}]
The :argument_id in each comment is a reference to the :id in the affirmatives/negatives
And here's the code that generates the modals.
(rf/reg-event-db
:app/show-modal
(fn [db [_ modal-id]]
(assoc-in db [:app/active-modals modal-id] true)))
(rf/reg-event-db
:app/hide-modal
(fn [db [_ modal-id]]
(update db :app/active-modals dissoc modal-id)))
(rf/reg-sub
:app/active-modals
(fn [db _]
(:app/active-modals db {})))
(rf/reg-sub
:app/modal-showing?
:<- [:app/active-modals]
(fn [modals [_ modal-id]]
(get modals modal-id false)))
(defn modal-card [id title body footer]
[:div.modal
{:class (when #(rf/subscribe [:app/modal-showing? id]) "is-active")}
[:div.modal-background
{:on-click #(rf/dispatch [:app/hide-modal id])}]
[:div.modal-card
[:header.modal-card-head
[:p.modal-card-title title]
[:button.delete
{:on-click #(rf/dispatch [:app/hide-modal id])}]]
[:section.modal-card-body
body]
[:footer.modal-card-foot
footer]]])
(defn modal-button [id title body footer]
[:div
[:button.button.is-primary
{:on-click #(rf/dispatch [:app/show-modal id])}
title]
[modal-card id title body footer]])

All your modals are passed the same id :argument. Thus, you always show the same modal, since the db lookup in your subscription is for :argument.
A minor tip: Following kebap-case is the recommended style in Clojure. So your app-db attributes should probably be argument-id and topic-id.
Furthermore, I would recommend subscribing debate-affirmatives and debate-negatives inside the components you pass them to. This would make them more self-containing.

Related

How to set array as state in Reagent using ClojureScript

I'm trying to create a state called items however my code gives an error and I'm unsure of why when I try to access the items and iterate over them. What am I doing wrong here?
(def items (r/atom ["test" "test2"]))
(defn home-page []
[:div#main
[:section.section
[:h1#s-one-greeting "Hello, I'm testing"]
[:h2#s-one-greeting-two "blah blah blah"]]
[:section.section
[:p "Work history"]
[:p "yada"]
[:p "yada"]
[:ul
(for [item items]
^{:key item} [:li "item " item])]]])
You are accessing items without derefing it. So you are trying to for loop over the reagent atom which doesn't work. Just switch it to (for [item #items] ...) and you should be fine.

reset! on reagent atom doesn't work as intended

So what I'm trying to do is a basic tabbed view using re-com's horizontal-tabs element. I added a v-box element and below that i want to have my tabs element and the body that corresponds to the tab. although on the :on-change i call reset! on the model of the horizontal-tabs and it doesn't seem to work.
(defn left-panel []
[re-com/box
:size "auto"
:child (let [selected-tab-id (r/atom (:id (first left-panel-tabs-definition)))
change-tab #(reset! selected-tab-id %)]
[re-com/v-box
:children [[re-com/horizontal-tabs
:model selected-tab-id
:tabs left-panel-tabs-definition
:on-change change-tab]
[(left-panel-tabs #selected-tab-id)]
]])])
(defn forms-view []
[:div "Forms View"])
(defn swagger-view []
[:div "Swagger View"])
(def left-panel-tabs
{::swagger #'swagger-view
::forms #'forms-view})
(def left-panel-tabs-definition
[{:id ::forms
:label "Forms"
:say-this "Forms View"}
{:id ::swagger
:label "Swagger"
:say-this "Swagger View"}])
If instead of
[(left-panel-tabs #selected-tab-id)]
i do something like
(do (log #selected-tab-id) [(left-panel-tabs #selected-tab-id)])
it'll always print the value that i've set my reagent atom at the beginning (in this case ::forms)
Thanks to u/Galrog over at reddit I realised that in order for this to work i need to create a Form-2 component (3 forms of creating a component http://reagent-project.github.io/docs/master/CreatingReagentComponents.html#the-three-ways)
Changing my left-panel component to a Form-2 component re-renders both children on the atom's change
Code:
(defn left-panel []
(let [selected-tab-id (r/atom (:id (first left-panel-tabs-definition)))
change-tab #(reset! selected-tab-id %)]
(fn []
[re-com/box
:size "auto"
:child [re-com/v-box
:children [[re-com/horizontal-tabs
:model selected-tab-id
:tabs left-panel-tabs-definition
:on-change change-tab]
[(left-panel-tabs u/selected-tab-id)]]]])))

Animate antizer table with rc-animate in re-frame app

I am trying to recreate the example in http://react-component.github.io/table/examples/animation.html to add animation to a table in a re-frame app. The table is rendered using antizer which is a ClojureScript library for Ant Design react components. For the animation I'm trying to use rc-animate (as in the example) which is a JavaScript library.
To integrate rc-animate, I followed the official Webpack guide and created a src/js/index.js file:
import Animate from 'rc-animate';
window.Animate = Animate;
My project.clj is:
(defproject ant-table-animation "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.10.238"]
[reagent "0.8.1"]
[re-frame "0.10.5" :exclusions [reagent]]
[antizer "0.3.1"]]
:plugins [[lein-cljsbuild "1.1.7"]]
:min-lein-version "2.5.3"
:source-paths ["src/clj" "src/cljs"]
:clean-targets ^{:protect false} ["resources/public/js/compiled" "target"]
:figwheel {:css-dirs ["resources/public/css"]}
:profiles
{:dev
{:dependencies [[binaryage/devtools "0.9.10"]
[cider/piggieback "0.3.9"]
[figwheel-sidecar "0.5.16"]
[day8.re-frame/re-frame-10x "0.3.3"]]
:plugins [[lein-figwheel "0.5.16"]]}
:prod { }
}
:cljsbuild
{:builds
[{:id "dev"
:source-paths ["src/cljs"]
:figwheel {:on-jsload "ant-table-animation.core/mount-root"}
:compiler {:closure-defines {re-frame.trace.trace_enabled_QMARK_ true}
:main ant-table-animation.core
:output-to "resources/public/js/compiled/app.js"
:output-dir "resources/public/js/compiled/out"
:asset-path "js/compiled/out"
:source-map-timestamp true
:preloads [devtools.preload, day8.re-frame-10x.preload]
:external-config {:devtools/config {:features-to-install :all}}
:infer-externs true
:npm-deps false
:foreign-libs [{:file "dist/index_bundle.js"
:provides ["rc-animate" "rc-animate-child"]
:global-exports {rc-animate Animate
rc-animate-child AnimateChild}}]
}}
{:id "min"
:source-paths ["src/cljs"]
:compiler {:main ant-table-animation.core
:output-to "resources/public/js/compiled/app.js"
:optimizations :advanced
:closure-defines {goog.DEBUG false}
:pretty-print false}}
]}
)
and in my views.cljs I try to render the table like this:
(ns ant-table-animation.views
(:require
[re-frame.core :as re-frame]
[ant-table-animation.subs :as subs]
[ant-table-animation.events :as events]
[antizer.reagent :as ant]
[reagent.core :as reagent]
[rc-animate]
))
(.log js/console rc-animate)
(defn AnimateBody
[props]
(.createElement
js/React
rc-animate
(.assign js/Object #js {:transitionName "move", :component "tbody"} props)))
(.log js/console AnimateBody)
(defn orders
[]
(let [orders #(re-frame/subscribe [::subs/orders])
width 80]
[ant/table
{:columns
[{:title "Product Name" :dataIndex :product :width width}
{:title "Quantity" :dataIndex :quantity :width width}
{:title "Unit Price" :dataIndex :price :width width}
{:title "Actions" :dataIndex "actions" :width width
:render
#(reagent/as-element
[ant/button
{:icon "delete" :type "danger"
:on-click
(fn []
(re-frame/dispatch [::events/order-deleted (.-product %2)]))}])}]
:dataSource orders
:size "small"
:components {:body {:wrapper AnimateBody}}
:pagination {:page-size 20}
:scroll {:y 300}}]))
(defn main-panel []
(let [name (re-frame/subscribe [::subs/name])]
[:div
[:h1 "Hello from " #name]
[orders]
]))
I am not sure at all about the
(defn AnimateBody
[props]
(.createElement
js/React
rc-animate
(.assign js/Object #js {:transitionName "move", :component "tbody"} props)))
being equivalent to the line from the example
const AnimateBody = props => <Animate transitionName="move" component="tbody" {...props} />;
The table renders correctly, but when I try to delete a row it fails with the following error trace:
react-dom.development.js:55 Uncaught Error: Unable to find node on an unmounted component.
at invariant (react-dom.development.js:55)
at findCurrentFiberUsingSlowPath (react-dom.development.js:4256)
at findCurrentHostFiber (react-dom.development.js:4266)
at findHostInstance (react-dom.development.js:17676)
at Object.findDOMNode (react-dom.development.js:18145)
at AnimateChild.transition (AnimateChild.js:83)
at AnimateChild.componentWillLeave (AnimateChild.js:70)
at performLeave (Animate.js:339)
at Array.forEach (<anonymous>)
at Animate.componentDidUpdate (Animate.js:188)
at commitLifeCycles (react-dom.inc.js:15386)
at commitAllLifeCycles (react-dom.inc.js:16646)
at HTMLUnknownElement.callCallback (react-dom.inc.js:143)
at Object.invokeGuardedCallbackDev (react-dom.inc.js:193)
at invokeGuardedCallback (react-dom.inc.js:250)
at commitRoot (react-dom.inc.js:16800)
at completeRoot (react-dom.inc.js:18192)
at performWorkOnRoot (react-dom.inc.js:18120)
at performWork (react-dom.inc.js:18024)
at performSyncWork (react-dom.inc.js:17996)
at requestWork (react-dom.inc.js:17884)
at scheduleWork (react-dom.inc.js:17689)
at Object.enqueueForceUpdate (react-dom.inc.js:11855)
at Object.Component.forceUpdate (react.inc.js:479)
at reagent$impl$batching$run_queue (batching.cljs?rel=1541330682770:39)
at Object.flush_queues (batching.cljs?rel=1541330682770:86)
at Object.run_queues (batching.cljs?rel=1541330682770:76)
at batching.cljs?rel=1541330682770:63
at re_frame_10x.cljs?rel=1541164419576:125
It is also indicated that:
The above error occurred in the <Animate> component:
in Animate (created by ant_table_animation.views.animateBody)
in ant_table_animation.views.animateBody (created by BaseTable)
in table (created by BaseTable)
in BaseTable (created by Connect(BaseTable))
in Connect(BaseTable) (created by BodyTable)
in div (created by BodyTable)
in BodyTable (created by ExpandableTable)
in div (created by ExpandableTable)
in div (created by ExpandableTable)
in div (created by ExpandableTable)
in ExpandableTable (created by Connect(ExpandableTable))
in Connect(ExpandableTable) (created by Table)
in Provider (created by Table)
in Table (created by LocaleReceiver)
in LocaleReceiver (created by Table)
in div (created by Spin)
in AnimateChild (created by Animate)
in div (created by Animate)
in Animate (created by Spin)
in Spin (created by Table)
in div (created by Table)
in Table (created by ant_table_animation.views.orders)
in ant_table_animation.views.orders (created by ant_table_animation.views.main_panel)
in div (created by ant_table_animation.views.main_panel)
in ant_table_animation.views.main_panel
I am a Clojure beginner, and I know even less for React; this is where I ended up after a week of trying but now I feel stuck. I have uploaded my project in github for anyone that would like to give it a try.
The Unable to find node on an unmounted component error was occurring because of version issues with React. I was able to handle it by explicitly using the version of React used by the rc-animate library - 16.5.2 in my project.clj:
...
[reagent "0.8.1" :exclusions [cljsjs/react cljsjs/react-dom [cljsjs/react-dom-server]]]
[cljsjs/react "16.5.2-0"]
[cljsjs/react-dom "16.5.2-0"]
[cljsjs/react-dom-server "16.5.2-0"]
...
[antizer "0.3.1" :exclusions [cljsjs/react cljsjs/react-dom [cljsjs/react-dom-server]]]]
...
For the correct definition of the AnimateBody component I had to use a combination of reagent/adapt-react-class, reagent/as-element and reagent/reactify-component.
Specifically, in my views.cljs I define the component as:
(def animate (reagent/adapt-react-class rc-animate))
(def animateBody
(fn [props]
(reagent/as-element [animate (assoc props :transition-name "move" :component "tbody")])))
and then pass it to the ant/table component with:
...
:components {:body {:wrapper (reagent/reactify-component animateBody)}}
...

Clojurescript react-leaflet does not display tiles properly

I am trying to use react-leaflet from ClojureScript but I have problem with the way tiles render:
some tiles do not display
there are tile showing next to each other in different cities
Here is the code I have:
(ns carder-devcards.map
(:require [taoensso.timbre :as timbre]
[cljsjs.react-leaflet] ;; js/ReactLeaflet
)
(:require-macros [devcards.core :as dc :refer [defcard]]))
(defn build
([component props]
(build component props (array)))
([component props & children]
(.createElement js/React
component
(clj->js props)
(array children))))
(def tile-layer (partial build js/ReactLeaflet.TileLayer))
(def leaflet-map (partial build js/ReactLeaflet.Map))
(def marker (partial build js/ReactLeaflet.Marker))
(def popup (partial build js/ReactLeaflet.Popup))
(defcard simple-leaflet
(fn [state]
(let [{:keys [pos zoom] :as st} #state
tl (tile-layer {:url "http://{s}.tile.osm.org/{z}/{x}/{y}.png"
:attribution "© OpenStreetMap contributors"})
mk (marker {:position pos})]
(leaflet-map {:center pos :zoom zoom}
tl
mk
)))
{:pos [51.505, -0.09]
:zoom 13}
{:header true})
And here is the result I have locally.
Note: resizing the browser seems to have an effect, so this could be a css problem as well (?). I have tried including the following without effect:
.leaflet-container {
height: 400px;
width: 100%;
}
The answer from #Chris Murphy set me up on the right track since I had the same error with it.
It turns out I was missing the css files of leaflet.js, including them resolved my problem.
Just posting my own very simple first cut leaflet on the off chance that it helps:
(def URL-OSM "http://{s}.tile.osm.org/{z}/{x}/{y}.png")
(defn create-map []
(let [m (-> js/L
(.map "mapid2")
(.setView (array -33.8675 151.2070) 9)) ;; Sidney
tile1 (-> js/L (.tileLayer URL-OSM
#js{:maxZoom 16
:attribution "OOGIS RL, OpenStreetMap ©"}))
base (clj->js {"OpenStreetMap" tile1})
ctrl (-> js/L (.control.layers base nil))]
(.addTo tile1 m)
(.addTo ctrl m)))
I am using [cljsjs/leaflet "0.7.7-4"].
Edit
And here's a leaflet-centric version of the markup:
(hiccup/html
[:head
[:meta {:charset "UTF-8"}]
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
[:link {:rel "stylesheet" :href "http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css" :type "text/css"}]
[:script {:src "http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js" :charset "utf-8"}]
[:body
[:div {:id "mapid"}]
[:div {:id "main-app-area"}]
[:script {:src "/reconnect/js/main.js" :type "text/javascript"}]])

Om Next read multi-fn not being called in second level join, Query AST not parsed fully, therefore component only receiving idents

I'm having trouble getting a second level join to work correctly. I've elided some things here for brevities sake.
My root component is:
(defui RootView
static om/IQuery
(query [this]
`[{:list/events ~(om/get-query Event)}])
Object
(render [this]
(let [{:keys [list/events]} (om/props this)]
(events/event-list events))))
My queries compose correctly and the initial data is normalised correctly. I won't show the normalised data and there's more to the total query.
(prn (om/get-query RootView)) =>
[{:list/events
[:id
{:body [:id :text :headline]}
{:media [:id :url :caption :credit]}
{:start-date [:id :year :month :day]}]}]
If I run a query containing the joins through a parser I get:
(prn (parser {:state (atom norm-data)}
'[{:list/events
[:id
{:body [:id :text :headline]}
{:media [:id :url :caption :credit]}
{:start-date [:id :year :month :day]}]}])) =>
{:list/events
[{:id 1,
:media [:media/by-id 1],
:start-date [:start-date/by-id 1],
:body [:body/by-id 1]}
{:id 17,
:media [:media/by-id 17],
:start-date [:start-date/by-id 17],
:body [:body/by-id 17]}]}
So the read function for :list/events is called and returns it's data, though all the second joins for :body, :media and :start-date are not.
My read functions are as follows, the second one is the one that is not called. I've left out the multi-methods on :media and :start-date, they also are not called. I'm not sure what this is a symptom of though.
(defmulti read om/dispatch)
(defmethod read :list/events
[{:keys [state] :as env} key params]
(let [st #state]
{:value (into [] (map #(get-in st %)) (get st key))}))
(defmethod read :body
[{:keys [state query]} key _]
(println "This is never printed")
{:value :doesnt-matter})
The join is correctly identified in the AST (so I assume the query grammar is correct) and the dispatch key matches that of the multi-method.
(prn (om/query->ast (om/get-query RootView))) =>
{:type :root,
:children
[{:type :join,
:dispatch-key :list/events,
:key :list/events,
:query
[:id
{:body [:id :text :headline]}
{:media [:id :url :caption :credit]}
{:start-date [:id :year :month :day]}],
:component timeline.components.events/Event,
:children
[{:type :prop, :dispatch-key :id, :key :id}
{:type :join,
:dispatch-key :body,
:key :body,
:query [:id :text :headline],
:component timeline.components.events/EventBody,
:children
[{:type :prop, :dispatch-key :id, :key :id}
{:type :prop, :dispatch-key :text, :key :text}
{:type :prop, :dispatch-key :headline, :key :headline}]}]}]}
I can't understand why the parser or something (?) stops at the second join? As far as my limited understanding goes, the multi-method on :body should at least be called?
So the issue I'm having is one of understanding I think, António Monteiro in the Om Slack channel suggested I use the db->tree function. Using this in the :list/events multi-method let's it return the whole tree of de-normalised data.
You have to do the recursion from within the reads yourself i.e. invoke the parser on the query that is within the key being examined. db->tree does this for you. In fact it is not unusual for every read to call db->tree and so look pretty much the same. In fact because of this Untangled does away with these reads altogether. In which case you really don't have to do the recursion yourself!
There's no recursion here:
(into [] (map #(get-in st %)) (get st key))
Any get on a key is to the refs part of the default db formatted data (app data). So here a sequence of idents will be returned by (get st key). Any get-in is to the tables part of the app data, and so returns real data values. (map #(get-in st %)) is the transducer that does this for every ident. But the tables part of the data is a recursive data structure - has to be for a lack of repetition - so any data that is not 'leaf' data is represented by an ident. So that's what you are getting back - anything that's one level deep and idents otherwise.
This answer is going to make next to no sense without an understanding of the default database format - the refs and tables parts. The best explanation I've found so far is here
This data (st) is in default db format:
{ :list/people [[:people/by-id 1] [:people/by-id 2] ... ]
:people/by-id { 1 { :db/id 1 :person/name "Joe" :person/mate [:people/by-id 2]}
2 { :db/id 2 :person/name "Sally" :person/mate [:people/by-id 1]}}}
Wherever you see by-id that's a give away that the key is in a tables mapentry. As you can see by the structure (get-in st [:people/by-id 1]) will retrieve for you a map that is the real data, of course only to one level deep.
Here :list/people is a key where the associated value is a vector of idents. (get st :list/people) will give you this vector. This mapentry is the refs part of st.