I have a problem using the Material UI's Autocomplete with Reagent (ClojureScript). The element renders fine, but when I try to click on it, I get the following exceptions:
Uncaught TypeError: Cannot read property 'focus' of null
at handleClick (useAutocomplete.js:938)
at HTMLUnknownElement.callCallback (react-dom.development.js:189)
at Object.invokeGuardedCallbackImpl (react-dom.development.js:238)
at invokeGuardedCallback (react-dom.development.js:293)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:307)
at executeDispatch (react-dom.development.js:390)
at executeDispatchesAndReleaseTopLevel (react-dom.development.js:412)
at forEachAccumulated (react-dom.development.js:3260)
at runEventsInBatch (react-dom.development.js:3305)
at handleTopLevel (react-dom.development.js:3515)
useAutocomplete.js:322 Uncaught TypeError: Cannot read property 'removeAttribute' of null
at eval (useAutocomplete.js:322)
at eval (useEventCallback.js:26)
at eval (useAutocomplete.js:433)
at eval (useEventCallback.js:26)
at eval (useAutocomplete.js:463)
at eval (useAutocomplete.js:528)
at commitHookEffectListMount (react-dom.development.js:19765)
at commitPassiveHookEffects (react-dom.development.js:19803)
at HTMLUnknownElement.callCallback (react-dom.development.js:189)
at Object.invokeGuardedCallbackImpl (react-dom.development.js:238)
Breaking in JS debugger, I see that inputRef.current is null (which is what focus and removeAttribute are called on. (Oddly enough, the only place where inputRef is set in the file is by calling useRef(null), which is what results in inputRef.current being null.)
In my code, I define the Autocomplete field as follows:
(ns my-product-redacted.views.atoms
(:require [reagent.core :as r]
["#material-ui/lab/Autocomplete" :default Autocomplete]
;; other requires
))
(def autocomplete-field (r/adapt-react-class Autocomplete))
Then, in a React component, it is used as follows:
[a/autocomplete-field {:render-input (fn [js-params]
(let [clj-params (js->clj js-params)
params {:label label
:width width
:select select?
:Input-label-props {:shrink true}
:Select-props {:native true}}
all-params (into clj-params params)]
(js/console.log (clj->js all-params))
(r/as-element [a/text-field all-params])))
:options (when select? (cons {:value "" :label ""} options))
:get-option-label (fn [option] (or (get (js->clj option) "label") ""))
:default-value (when (not select?) value-override)
:value (when select? value)
:disabled disabled?
:on-focus #(re-frame/dispatch [::forms/on-focus path])
:on-blur #(re-frame/dispatch [::forms/on-blur path])
:on-change #(re-frame/dispatch (conj on-change (-> % .-target .-value)))})]
(Here, a/text-field is also defined in the same namespace as a/autocomplete-field and in a similar way.)
The JS console log (from the (js/console.log (clj->js params)) call) shows that inputProps.ref.current is set to null. However, InputProps.ref is not null. Even so, I tried to manually associate the same function as is passed with InputProps.ref to inputProps.ref.current, but that made no difference.
I have also tried the workaround suggested in https://github.com/mui-org/material-ui/issues/21245 (although that issue is with the Gestalt library, not with Reagent, it suggests that there may be a problem with ref forwarding). But wrapping the text-field into a div with the ref taken from InputProps.ref also made no difference.
Any suggestions?
Calling js->clj on the render-input js params breaks them.
I put a quick demo of the material UI autocomplete component in my demo repository here.
But it's mainly taken from the official reagent docs/examples here:
(defn autocomplete-example []
[:> mui/Grid
{:item true}
[:> Autocomplete {:options ["foo" "bar" "foobar"]
:style {:width 300}
;; Note that the function parameter is a JS Object!
;; Autocomplete expects the renderInput value to be function
;; returning React elements, not a component!
;; So reactify-component won't work here.
:render-input (fn [^js params]
;; Don't call js->clj because that would recursively
;; convert all JS objects (e.g. React ref objects)
;; to Cljs maps, which breaks them, even when converted back to JS.
;; Best thing is to use r/create-element and
;; pass the JS params to it.
;; If necessary, use JS interop to modify params.
(set! (.-variant params) "outlined")
(set! (.-label params) "Autocomplete")
(r/create-element mui/TextField params))}]])
Related
how to rerender the whole reagent tree when we save file and shadow-cljs reloads?
react 18 has new createRoot api
and even before - if nothing changed calling render has no effect
With react v18, you need to create the root node only once. After this, you can call the .render() function from it to (re-)render your application.
Also, you need to configure a function / behavior to tell shadow-cljs what it should do on during reload.
Here is a full example, taken from here https://github.com/schnaq/cljs-reagent-template
(ns playground
(:require ["react-dom/client" :refer [createRoot]]
[goog.dom :as gdom]
[reagent.core :as r]))
(defn- main []
[:main.container.mx-auto
[:h1 "Welcome to your app"]])
;; -----------------------------------------------------------------------------
(defonce root (createRoot (gdom/getElement "app")))
(defn init
[]
(.render root (r/as-element [main])))
(defn ^:dev/after-load re-render
[]
;; The `:dev/after-load` metadata causes this function to be called
;; after shadow-cljs hot-reloads code.
;; This function is called implicitly by its annotation.
(init))
shadow-cljs is configured to call the init-function from playground/init.
;; shadow-cljs.edn
{...
:builds {:frontend {:modules {:main {:init-fn playground/init}}}}}
https://github.com/move-me-to-ipfs-shipyard/Fennec/blob/bbfb566211041dd57b419ffd95f642026bb989a8/src/Fennec/ui.cljs#L263
(:require
["react-dom/client" :as Pacha.dom.client]
[reagent.core :as Kuzco.core])
; seed.cljs
(defonce root {:dom-rootA (atom (Pacha.dom.client/createRoot
(.getElementById js/document "ui")))})
; ui.cljs
(:require [Fennec.seed :refer [root]])
(defn reload
[]
(when-let [dom-root #(:dom-rootA root)]
(.unmount dom-root)
(let [new-dom-root (Pacha.dom.client/createRoot
(.getElementById js/document "ui"))]
(reset! (:Pacha-dom-rootA root) new-dom-root)
(.render #(:Pacha-dom-rootA root)
(Kuzco.core/as-element [rc-current-page])))))
I'm trying to use the React Simulate function to simulate a mouseDown event for testing.
(defn mouse-down [node]
((.-mouseDown(.-Simulate ReactTestUtils) node (clj->js {:button 0})))
js translation:
ReactTestUtils.Simulate.mouseDown(node, {button: 0})
Nothing I've tried has resulted in an invocation of the mousedown listener--but when the listener is there when i try it in the browser where it works. It's just in the simulation.
What am I missing?
There are a couple mistakes in the Syntax here and your parens don't match. Generally it becomes easier if you reorganize the code a bit and -> can help there. As mentioned in the comment you want to use .mouseDown instead of .-mouseDown. I opted to use #js instead of clj->js since that is more optimal for static js objects such as this.
(defn mouse-down [node]
(-> (.-Simulate ReactTestUtils)
(.mouseDown node #js {:button 0})))
You can also make this a little more readable depending on where ReactTestUtils is coming from. I'm assuming its from the react-dom package which you just required.
;; in your ns
(:require ["react-dom/test-utils" :as ReactTestUtils])
;; which would allow
(defn mouse-down [node]
(ReactTestUtils/Simulate.mouseDown node #js {:button 0}))
;; or if you are on the latest CLJS version
(:require ["react-dom/test-utils$Simulate" :as sim])
;; and then
(defn mouse-down [node]
(sim/mouseDown node #js {:button 0}))
I've crawled through some of the web and video documentation for routing. But I'm failing to get Dynamic UI Routing working for a simple set of pages.
root.cljs
(ns ui.root)
;; ...
(defsc Index [this props]
{:query [:index]
:ident (fn [] [:id :index])
:route-segment ["index"]
:initial-state {}}
(h3 "Index"))
(defsc Landing [this props]
{:query [:landing]
:ident (fn [] [:id :landing])
:route-segment ["landing"]
:initial-state {}}
(h3 "Landing"))
(defsc Settings [this props]
{:query [:settings]
:ident (fn [] [:id :settings])
:route-segment ["settings"]
:initial-state {}}
(h3 "Setting"))
(dr/defrouter TopRouter [this {:keys [current-state] :as props}]
{:router-targets [Game Settings Landing Index]
:initial-state (fn [{:keys [current-state]}]
{:current-state current-state})}
(case current-state
:pending (dom/div "Loading...")
:failed (dom/div "Failed!")
(dom/div "No route selected.")))
(def ui-top-router (comp/factory TopRouter))
(defsc Root [this {:keys [router] :as props}]
{:query [{:router (comp/get-query TopRouter)}]
:ident (fn [] [:id :root])
:initial-state (fn [_]
{:top-router (comp/get-initial-state TopRouter {:current-state :pending})
:index {:id 1}
:landing {:id 1}
:settings {:id 1}})
:componentDidMount (fn [_] (log/info "Root Initial State /" (prim/get-initial-state Root {})))}
(log/info "Root Props /" this props)
(ui-top-router router {:current-state :pending}))
client.cljs
(ns client)
...
(app/mount! #app root/Root "app" {:initialize-state? true
:foo :bar})
Q: Initial load gives this output. How do we pass props into the Root component? I expect to see at least {:foo :bar}.
INFO [ui.root:81] - Root Props / [object Object] {}
INFO [ui.root:53] - TopRouter Props / {:current-state nil, :route-factory #object[cljs.core.MetaFn], :pending-path-segment nil, :route-props nil}
INFO [ui.root:77] - Root Initial State / nil
Q: If this is my initial state, is the :query and :ident right? And do they (:query + :ident) correspond to the :route-segment ? Do they need to?
{:index {:id 1}
:landing {:id 1}
:settings {:id 1}}
Q: How do we kick off the initial route? Calling this fails with the below message.
(dr/change-route app ["index"])
INFO [com.fulcrologic.fulcro.rendering.ident-optimized-render:146] - Optimized render failed. Falling back to root render.
>> UPDATE <<
I was able to get a working Fulcro Root :initial-state, and :query and :ident on child components.
On initial load, the router fails with this.
INFO [beatthemarket.ui.root:61] - TopRouter Props / {:current-state nil, :route-factory #object[cljs.core.MetaFn], :pending-path-segment nil, :route-props {:index/id 1, :index/text "Index Text"}}
core.cljs:159 ERROR [com.fulcrologic.fulcro.routing.dynamic-routing:410] - will-enter for router target beatthemarket.ui.root/Index did not return a valid ident. Instead it returned: [:index/id nil]
core.cljs:159 ERROR [com.fulcrologic.fulcro.routing.dynamic-routing:410] - will-enter for router target beatthemarket.ui.root/Index did not return a valid ident. Instead it returned: [:index/id nil]
browser.cljs:25 shadow-cljs: WebSocket connected!
browser.cljs:25 shadow-cljs: REPL session start successful
core.cljs:159 INFO [com.fulcrologic.fulcro.algorithms.indexing:104] - component beatthemarket.ui.root/Index's ident ([:index/id nil]) has a `nil` second element. This warning can be safely ignored if that is intended.
So a command like (dr/change-route app (dr/path-to root/Index)) fails with this.
react_devtools_backend.js:6 ERROR [com.fulcrologic.fulcro.routing.dynamic-routing:410] - will-enter for router target beatthemarket.ui.root/Index did not return a valid ident. Instead it returned: [:index/id nil]
These are my client.cljs and root.cljs look like this.
I think your Root initial state should be calling (comp/get-initial-state Index). You have an initial state set on Index but it's different than the initial state that Root gives.
Also, a big part of Fulcro (and React) is that you build a tree of components and a tree of data and they need to match.
The way you have it here, there's no connection between "Root" and "Index" because Root only renders (ui-top-router router). You're getting the data for Index by having a query for {:root/index (comp/get-query Index)} but you're not creating the connection between Root and Index by having Root call Index and pass in that data. You need a (ui-index index) inside Root.
And if you do that, then in that (ui-index index) call, index will take the value that you're setting with :initial-state. That's why you'll also need to update initial-state to call comp/get-initial-state so that you can get the :index/id 1 value that you're setting in the Index component's initial state.
(defsc Index [this {:index/keys [id text]}]
{:query [:index/id :index/text]
:ident [:index/id :index/id]
:route-segment ["index"]
:initial-state {:index/id 1
:index/text :param/text}}
(h3 text))
(defsc Root [this {:root/keys [router index landing game settings]}]
{:query [{:root/router (comp/get-query TopRouter)}
{:root/index (comp/get-query Index)}
{:root/landing (comp/get-query Landing)}
{:root/game (comp/get-query Game)}
{:root/settings (comp/get-query Settings)}]
:initial-state {:root/router {}
:root/index {:text "Index Text"}
:root/landing {:text "Landing Text"}
:root/game {:text "Game Text"}
:root/settings {:text "Settings Text"}}}
(when router
(dom/div (ui-top-router router))))
After all of that is addressed, here's the next thing you might be interested in.
You probably won't always want to hard-code index/id 1. You might need to fetch data from a server in order to have anything to render.
That's where :will-enter and "deferred routing" come into play. See the example below and the docs at http://book.fulcrologic.com/#_router_rendering_of_a_deferred_ui.
(defsc Person [this {:ui/keys [modified?]
:person/keys [id name]
:address/keys [city state]
:as props}]
{:query [:ui/modified? :person/id :person/name :address/city :address/state]
:ident :person/id
:route-segment ["person" :person/id]
:route-cancelled (fn [{:person/keys [id]}]
(log/info "Routing cancelled to user " id))
:allow-route-change? (fn [this {:ui/keys [modified?]}]
(when modified?
#?(:cljs (js/alert "You cannot navigate until the user is not modified!")))
(not modified?))
:will-enter (fn [app {:person/keys [id] :as route-params}]
(log/info "Will enter user with route params " route-params)
;; be sure to convert strings to int for this case
(let [id (if (string? id) (edn/read-string id) id)]
(dr/route-deferred [:person/id id]
#(df/load app [:person/id id] Person
{:post-mutation `dr/target-ready
:post-mutation-params {:target [:person/id id]}}))))}
I guess you first have to fix the Ident problem with your Index component.
Does routing work for the other components?
Suppose having
(def defining-list `(def one 1))
How can I evaluate defining-list so that one becomes 1 ?
(in clojurescript)
EDIT:
I will give an idea of the broader image and what I am trying to accomplish here to avoid falling into an X/y problem.
I am trying to use cljsjs/material-ui from cljsjs package
Instead of defining each time a react component to use it as following:
(def app-bar
(r/adapt-react-class (aget js/MaterialUI (name :AppBar)))
I would like to define all the components from an array of tags:
(def material-ui-tags '[AppBar Avatar Backdrop])
So I was thinking if it's possible to do this without the usage of a macro as I found this
Something like:
(doseq [component material-ui-tags]
`(def ~(symbol (->kebab-case component)) (r/adapt-react-class (aget js/MaterialUI ~(name component)))))
But the above does only create a list of defs, I would like to evaluate these. In clojure eval would do the trick.
With reagent, you can use :> as shorthand for adapt-react-class as documented in https://github.com/reagent-project/reagent/blob/master/docs/InteropWithReact.md
Also, you can use dot notation with js/ and I think in shadow-cljs or cljs above 1.9.854 you can require to import the symbol instead of using aget.
In your case, it would be something like:
(ns example.core
(:require [MaterialUI]))
(defn component-two []
[:> MaterialUI/AppBar {:some-prop "some-value"}
[:div "children-here"]])
(defn component-two []
;; If the require above doesn't work
[:> js/MaterialUI.AppBar {:some-prop "some-value"}
[:div "children-here"]])
To do what you wanted using def, you either need eval or macro. Eval is not ideal as Jared Smith explained in the comment.
The example that you linked from reagent-material-ui uses macro. Invoking a macro actually performs expansion and then evaluation. So your code needs to be something like this:
clj file
(def material-ui-tags '[AppBar Avatar Backdrop])
(defmacro adapt-components []
(for [component material-ui-tags]
`(def ~(symbol (->kebab-case component)) (reagent.core/adapt-react-class (aget js/MaterialUI ~(name component))))))
cljs file
(adapt-components) ;; your defs will be available below this line
(defn my-component []
[app-bar ...])
In Om, is there a way to add a class to an element based on some value in the main app atom?
Take the following element, for example:
(defn some-component [app owner]
(reify
om/IRender
(render
[_]
(html
[:div {:class (when (:some-key app) "awesomeclass")} "Some text!"]))))
If I somehow toggle the value of :some-key between true and false, the class doesn't get added or removed. The value of :some-key on page load determines whether or not the class gets added.
I am using this idea to conditionally show/hide (using Twitter Bootstrap's hide class) menu options based on the value of the :access key in my app state:
[:ul.dropdown-menu {:role "menu"}
[:li {:class (when (:access app) "hide")} [:a.pointer {:on-click #(om/update! app :view :login)} "Login"]]
[:li {:class (when (:access app) "hide")} [:a.pointer {:on-click #(om/update! app :view :register)} "Register"]]
[:li {:class (when-not (:access app) "hide")} [:a.pointer {:on-click #(om/update! app :view :dashboard)} "Dashboard"]]
[:li {:class (when-not (:access app) "hide")} [:a.pointer {:on-click #(om/update! app :view :settings)} "Settings"]]
[:li [:a.pointer {:on-click #(om/update! app :view :about)} "About"]]]
This code works as expected. When I click on "Click Me!", while looking at the Elements in Developer Tools (Chrome), I see the class toggling between "awesomeclass" and disappearing:
(defonce app-state (atom {:some-key true}))
(defn main []
(om/root
(fn [app owner]
(reify
om/IRender
(render [_]
(html
[:div {:class (when (:some-key app) "awesomeclass")}
"Some text!"
[:a {:on-click #(om/transact! app :some-key not)}
"Click Me!"]]))))
app-state
{:target (. js/document (getElementById "app"))}))
As rojoca pointed out, the problem might be in the way you update app-state, consider using om/transact! and om/update!. If you keep having problems, post the code that updates the state.
Note: I'm running this in Chestnut.
You should use the keyword :className instead of :class.