No state info with Chrome React.js tools - clojurescript

I have the following ClojureScript code that uses the om library as a wrapper to React.js
(defn list-view [app owner]
(reify
om/IInitState
(init-state [_]
{:filter nil
:selected-domain nil})
om/IWillMount
(will-mount [_]
(th/poll filter-chan (fn[data]
(om/set-state! owner :filter data))))
om/IRenderState
(render-state [this state]
(let [list-data (sort (list-data (:list app) (:filter state)))]
(if (> (count list-data) 0)
(dom/div #js {:className "sidebar-module sidebar-module-inset"}
(dom/div #js {:className "bs-example well"}
(apply dom/ul #js {:className "list-group"}
(map (fn [text] (domain-list-item text (:selected-domain state) owner))
list-data))))
(dom/span nil))))))
This are the helper functions used in the code above
(defn list-data [alist filter-text ]
(filter (fn [x] (cond (empty? filter-text) false
(nil? filter-text) false
(= filter-text "*") true
:else (> (.indexOf (.toLowerCase x) filter-text) -1))) alist))
(defn domain-list-item [text selected owner]
(let [class-name (str "list-group-item" (if (= text selected) " isSelected" ""))]
(dom/li #js {:className class-name}
(dom/a #js
{:href "#"
:onClick (fn [event] (select-domain owner text))} text))))
Everything works as expected. The only thing that bothers me Is that I do not see any state info when I analyze the page with the React.js tools in Chrome.

It seems as if sometimes the state is directly visible in the state area (e.g. for an input element) and sometimes it is hidden inside the __om_state object.

I find that I often have to click on another component and then back on the component I'm interested in to see the state in the state area. Perhaps that's the issue here?

Related

Problem opening files with the FileReader API

This has been bugging me for days. I have a web app that lets the user open documents from their local machine. I'm using the FileReader API for the first time.
It works correctly except for one use case.
Open a document file.
Programmatically create a new document, overwriting the existing one.
Open the same file as above.
When this sequence is executed, the second attempt fails silently (except that the file is not loaded).
Here is an example Reagent program (created from the figwheel-main template) that illustrates the problem.
(ns a-bad-button.core
(:require [reagent.core :as r]))
(def app-state-ratom (r/atom nil))
(defn new-doc []
{:doc-text "Some MINIMAL text to play with."})
(defn add-new-button
[aps]
(fn [aps]
[:input.tree-demo--button
{:type "button"
:value "New"
:on-click #(reset! aps (new-doc))}]))
(defn load-doc-data!
[aps file-data]
(swap! aps assoc :doc-text file-data))
(defn handle-file-open-selection
[aps evt]
(let [js-file-reader (js/FileReader.)]
(set! (.-onload js-file-reader)
(fn [evt] (load-doc-data! aps (-> evt .-target .-result))))
(.readAsText js-file-reader (aget (.-files (.-target evt)) 0))))
(defn add-open-button
[aps]
(fn [aps]
[:div
[:input {:type "file" :id "file-open-id"
:style {:display "none"}
:on-change #(handle-file-open-selection aps %)}]
[:input {:type "button"
:value "Open"
:on-click #(.click (.getElementById js/document "file-open-id"))}]]))
(defn a-bad-button
[aps]
(fn [aps]
[:div
[:h4 "A Bad Button"]
[:p#doc-text-p (or (:doc-text #aps) "Loaded text will go here.")]
[add-new-button aps]
[add-open-button aps]]))
(defn mount! [el]
(reset! app-state-ratom (new-doc))
(r/render-component [a-bad-button app-state-ratom] el))
(defn mount-app-element []
(when-let [el (.getElementById js/document "app")]
(mount! el)))
(mount-app-element)
(defn ^:after-load on-reload []
(mount-app-element))
With println debugging messages, it appears that execution reaches the :on-click handler in the add-open-button function, but the handler, handle-file-open-selection, is never reached or executed.
The failure occurs on Safari, Opera, Brave, and Vivaldi browsers. Files open as expected on Firefox.
Has anyone seen this before and fixed it?
Similar questions:
Filereader - upload same file again not working
FileReader onload not getting fired when selecting same file in Chrome
Basically, the problem is that onChange will not trigger when selecting the same file. One workaround is to set the value of the file input before the file browser opens to something like "", to always trigger an onChange event. In your case, it could look like changing your handle-file-open-selection function to:
(defn handle-file-open-selection
[aps evt]
(let [js-file-reader (js/FileReader.)]
(set! (.-onload js-file-reader)
(fn [evt]
(load-doc-data! aps (-> evt .-target .-result))))
(.readAsText js-file-reader (aget (.-files (.-target evt)) 0))
;; add this
(set! (.-value (.getElementById js/document "file-open-id")) "")
))

Om ref cursor not re-rendering components when updated

(ns ^:figwheel-always refs-test.core
(:require [om.core :as om :include-macros true]
[om.dom :as dom :include-macros true]
[sablono.core :as html :refer-macros [html]]))
(enable-console-print!)
(def app-state
(atom {:items [{:text "cat"}
{:text "dog"}
{:text "bird"}]
:selected-item {}}))
(defn selected-item []
(om/ref-cursor (:selected-item (om/root-cursor app-state))))
(defn
selected-item-title
[_ owner]
(reify
om/IRender
(render [_]
(html
[:div
(let [selected (om/observe owner (selected-item))]
(if (empty? selected)
[:h1 "Nothing selected"]
[:h1 (:text selected)]))]))))
(defn
selected-item-button
[item owner]
(reify
om/IRender
(render [_]
(html
[:li
[:button {:on-click
(fn []
(om/update! (om/root-cursor app-state) :selected-item item) ;; this doesn't update
;;(om/update! (om/root-cursor app-state) :selected-item (merge item {:foo 1})) ;; this does
)} (:text item)]]))))
(defn
root
[cursor owner]
(reify
om/IRender
(render [_]
(html
[:div
(om/build selected-item-title {})
[:ul
(om/build-all selected-item-button (:items cursor))]]))))
(om/root root app-state
{:target (.getElementById js/document "app")})
(https://www.refheap.com/108491)
The (selected-item) function crerates a ref-cursor which tracks the :selected-item key in app-state. When you click a selected-item-button the title changes to reflect the new value that has been put into the map. However, this only works once. Pressing a different button does not cause the title to re-render again so the title is always stuck at the value of the first button you pressed.
Although, simply adding a merge with an additional keyword seems to make it work... (merging with an empty map doesn't work either, tried that!)
Is my understanding on ref cursors wrong?
So, the issue was very simple.
(om/update! (om/root-cursor app-state) :selected-item item)
should have been
(om/update! (om/root-cursor app-state) :selected-item #item)
Notice the item, because it's a cursor, is dereferenced.

Om app-state and application structure

I'm displaying a menu in Om, using a component and subcomponent like this:
(def app-state (atom {:location ""
:menuitems [["Pages" "/pages/"]
["Images" "/images/"]]}))
(defn menu-item-view [parent-cursor item owner]
(reify
om/IRender
(render [this]
(dom/li #js {:className (if (= (:location #app-state) (last item)) "active" "inactive")}
(dom/a #js
{:onClick (fn [_] (swap! app-state assoc :location (last #item)))}
(first item))))))
(defn menu-view [app owner]
(reify
om/IRender
(render [this]
(dom/li #js {:className "has-dropdown not-click"}
(dom/a nil "Menu")
(apply dom/ul #js {:className "dropdown"}
(om/build-all (partial menu-item-view app)
(:menuitems app)))))))
(om/root menu-view app-state
{:target (. js/document (getElementById "menu"))})
My question is how do I update the (#app-state :location) and correctly rerender the menu?
The update in the code above:
(swap! app-state assoc :location (last #item))
does work, but the tree is not updated correct.
I suspect i need to use om/update! or om/transact! but they take a cursor and the only cursor i have in menu-item-view is to the current menu item, not the full app-state. So i cannot access :location.
How is this handled?
I would prefer to aviod core.async and channels for the time being if possible.
Now that we have reference cursors you could probably do something like this:
(def app-state (atom {:location ""
:menuitems [["Pages" "/pages/"]
["Images" "/images/"]]}))
(defn location []
(om/ref-cursor (:location (om/root-cursor app-state))))
(defn menu-item-view [item owner]
(reify
om/IRender
(render [this]
(let [x (location)]
(dom/li #js {:className (if (= x (last item)) "active" "inactive")}
(dom/a #js
{:onClick (fn [_] (om/update! x (last #item)))}
(first item)))))))
(defn menu-view [app owner]
(reify
om/IRender
(render [this]
(dom/li #js {:className "has-dropdown not-click"}
(dom/a nil "Menu")
(apply dom/ul #js {:className "dropdown"}
(om/build-all menu-item-view (:menuitems app)))))))
(om/root menu-view app-state
{:target (. js/document (getElementById "menu"))})
It's just an idea - I haven't actually tested it.
Yes, all updates should occur through om/transact! or om/update!.
You could pass the main cursor to the controls state in :init-state or :state. This would give you access to it for update.
Alternatively, you could avoid using om/build-all and use build directly to pass multiple cursors to the control as specified here.
Simply call the following instead:
(map #(om/build menu-item-view {:main-cursor app :menu-cursor %}) (:menuitems app))

Cannot manipulate cursor outside of render phase

Trying react for the first time, and I want to make a simple todo list app. But every time I press enter to trigger onSubmit it says Uncaught Error: Cannot manipulate cursor outside of render phase, only om.core/transact!, om.core/update!, and cljs.core/deref operations allowed. While I think this is a very good error message, I don't know what to do.
(ns app.core
(:require [om.core :as om :include-macros true]
[sablono.core :as html :refer-macros [html]]))
(def app-state (atom
{:todos [{:todo "first"}
{:todo "second"}]
:current ""}))
(defn to-do
[data]
(om/component
(html [:li (:todo data)])))
(defn to-dos
[data]
(om/component
(html [:div
[:form {:on-submit (fn [e]
(.preventDefault e)
(om/transact! data :todos (fn [v]
(js/console.log (:current data))
(conj v (:current data)))))}
[:input {:type "text"
:placeholder "Enter some text."
:on-change (fn [e] (om/update! data :current (.. e -target -value)))}]]
[:ul
(om/build-all to-do (:todos data))]])))
(om/root to-dos app-state {:target js/document.body})
I think the problem is where you access data inside om/transact! where you should operate on v:
(:current v) instead of (:current data)
or you may try (:current #data) for most recent value of data
There are actually two issues with:
(om/transact! data :todos (fn [v]
(js/console.log (:current data))
(conj v (:current data)))))
One is what #edbond said above: you should use (:current v) rather than (:current data). The other problem, however, is that you are specifying the :todos keyword, and instead, you should simply change data itself, since :current is outside of :todos in your app-state shown. So the correct formulation would be:
(om/transact! data (fn [v]
(js/console.log (:current v))
(conj v (:current v)))))

Clojurescript this-as macro points to global object

During writting reactjs tutorial in clojurescrtipt i've found that this-as macro compiles to
(function(){var t = this; return t;}
which always points to window inside react classes. Sometimes i can workaround this by js* this but not inside let or map cuz they are also compiled to functions.
How can i access react js this inside let form?
Situation on fiddle: http://jsfiddle.net/VkebS/57/
and a piece of tutorial FYI:
(def comment-list
(React/createClass
#js{:render
(fn [] (dom/div #js {:className "commentList"}
(let [d (this-as t (.. t -props -data))]
(map #(commnt #js {:author (:author %)} (:text %)) d))))}))
PS: i can use native array for data and native map function
(def comment-list
(React/createClass
#js{:render
(fn [] (dom/div #js {:className "commentList"}
(.map (.. (js* "this") -props -data) #(commnt #js {:author (:author %)} (:text %)))))}))
that works, but...
this-as works if you use it at start of render function:
(def commnt
(React/createClass
#js {:render
(fn []
(this-as this
(dom/div #js {:className "comment"}
(dom/h2 #js {:className "commentAuthor"}
(.. this -props -author))
(dom/span #js {:dangerouslySetInnerHTML
#js{:__html
(.makeHtml converter (.. (js* "this") -props -children toString))}}))))}))
see also: https://github.com/swannodette/om/blob/master/src/om/dom.cljs#L34