Normalization and Idents in Om/Next - clojurescript

I’m trying to get my head around normalization and thought I was making progress, but I’ve stumbled again and am not sure if I’m just not thinking correctly about the problem. How do I normalize the current user’s messages?
(def init-data
{:session {:user/id 1
:messages [{:message/id 1}]}
:messages [{:message/id 1 :text "Message 1"}
{:message/id 2 :text "Message 1"}]
:users [{:user/id 1 :email "1#foo.com"}
{:user/id 2 :email "2#foo.com"}]})
(defui Message
static om/Ident
(ident [this {:keys [message/id]}]
[:message/by-id id])
static om/IQuery
(query [this]
[:id]))
(defui User
static om/Ident
(ident [this {:keys [user/id]}]
[:user/by-id id])
static om/IQuery
(query [this]
`[:id {:properties ~(om/get-query Property)}]))
(defui Session
static om/Ident
(ident [this {:keys [user/id]}]
[:user/by-id id])
static om/IQuery
(query [this]
[:id]))
(defui RootView
static om/IQuery
(query [this]
(let [message-query (om/get-query Message)
user-query (om/get-query User)
session-query (om/get-query Session)]
`[{:messages ~message-query}
{:users ~user-query}
{:session ~session-query}])))
=> (def norm-data (om/tree->db RootView init-data true))
=> (pp/pprint norm-data)
{:session [:user/by-id 1],
:messages [[:message/by-id 1] [:message/by-id 2]],
:users [[:user/by-id 1] [:user/by-id 2]],
:message/by-id
{1 {:message/id 1, :text "Message 1"},
2 {:message/id 2, :text "Message 1"}},
:user/by-id
{1 {:user/id 1, :email "1#foo.com", :messages [{:message/id 1}]},
2 {:user/id 2, :email "2#foo.com"}},
:om.next/tables #{:message/by-id :user/by-id}}

I changed your initial data a little and managed to get tree->db to get us to a sensible looking default-db-format, where Idents are ubiquitous:
{:app/session [:session/by-id 1],
:app/messages [[:message/by-id 100] [:message/by-id 101]],
:app/users [[:user/by-id 200] [:user/by-id 201]],
:message/by-id
{100 {:id 100, :text "Message 1"}, 101 {:id 101, :text "Message 2"}},
:user/by-id
{200 {:id 200, :email "1#foo.com"},
201 {:id 201, :email "2#foo.com"}},
:session/by-id {1 {:id 1, :app/messages [[:message/by-id 100]]}}}
The components:
(defui Message
static om/Ident
(ident [this {:keys [id]}]
[:message/by-id id])
static om/IQuery
(query [this]
[:id :text]))
(defui User
static om/Ident
(ident [this {:keys [id]}]
[:user/by-id id])
static om/IQuery
(query [this]
[:id :email]))
(defui Session
static om/Ident
(ident [this {:keys [id]}]
[:session/by-id id])
static om/IQuery
(query [this]
[:id {:app/messages (om/get-query Message)}]))
(defui RootView
static om/IQuery
(query [this]
[{:app/messages (om/get-query Message)}
{:app/users (om/get-query User)}
{:app/session (om/get-query Session)}]))
And the initial data (the input to tree->db):
(def init-data
{:app/session {:id 1
:app/messages [{:id 100}]}
:app/messages [{:id 100 :text "Message 1"}
{:id 101 :text "Message 2"}]
:app/users [{:id 200 :email "1#foo.com"}
{:id 201 :email "2#foo.com"}]})

Related

How can I prevent the closure compiler from minifying certain methods in clojurescript?

I'm integrating quilljs with my clojurescript application. I'm including it in my project.cljs file like so: [cljsjs/quill "1.3.5-0"].
The compiler is minifying some methods and is causing an error:
function xA(a, b) {
var c = t(a).getSelection(!0)
, d = c.index
, e = c.length
, h = Quill.import("delta");
c = function(b, c, d, e) {
return function(b) {
return t(a).updateContents((new e).rf(c).delete(d).nf({
image: b
}))
}
}(c, d, e, h);
return b.c ? b.c(c) : b.call(null, c)
}
This is the error: Uncaught TypeError: (intermediate value).rf is not a function
The clojurescript code looks like this:
(defn file-recieve-handler [this cb]
(let [range (.getSelection #this true)
index (.-index range)
length (.-length range)
delta (.import js/Quill "delta")]
(cb (fn [url]
(.updateContents #this
(.insert
(.delete
(.retain (new delta) index)
length)
#js {:image url}))))))
The retain method and the insert method are getting minified - and they shouldn't be. (The delete is not for some reason, I'm guessing that's because it's a keyword in javascript.)
I found the externs file for quilljs:
https://github.com/cljsjs/packages/blob/master/quill/resources/cljsjs/quill/common/quill.ext.js
Is there someway I need to supplement the extern file or another way I can write the code so those two methods don't get minified when advanced compilation is turned on for the compiler?
For some context below is the full file. It's based on this https://github.com/benhowell/reagent-quill/blob/master/quill.cljs
(ns quill.core
(:require
[reagent.core :as r]))
(defn quill-toolbar [id]
[:div {:id (str "quill-toolbar-" id)}
[:span {:class "ql-formats"}
[:select {:class "ql-header"}
[:option {:value "1"}]
[:option {:value "2"}]
[:option {:value "3"}]
[:option {:value "4"}]
[:option {:value "5"}]
[:option]]]
[:span {:class "ql-formats"}
[:select {:class "ql-font"}
[:option]
[:option {:value "serif"}]
[:option {:value "monospace"}]]]
[:span {:class "ql-formats"}
[:select {:class "ql-size"}
[:option {:value "small"}]
[:option]
[:option {:value "large"}]
[:option {:value "huge"}]]]
[:span {:class "ql-formats"}
[:button {:class "ql-bold"}]
[:button {:class "ql-italic"}]
[:button {:class "ql-underline"}]
[:button {:class "ql-strike"}]
[:button {:class "ql-blockquote"}]]
[:span {:class "ql-formats"}
[:select {:class "ql-align"}]]
[:span {:class "ql-formats"}
[:button {:class "ql-script" :value "sub"}]
[:button {:class "ql-script" :value "super"}]]
[:span {:class "ql-formats"}
[:button {:class "ql-indent" :value "-1"}]
[:button {:class "ql-indent" :value "+1"}]]
[:span {:class "ql-formats"}
[:button {:class "ql-image"}] ]
[:span {:class "ql-formats"}
[:select {:class "ql-color"}]
[:select {:class "ql-background"}]]
[:span {:class "ql-formats"}
[:button {:class "ql-clean"}]]])
(defn file-recieve-handler [this cb]
(let [range (.getSelection #this true)
index (.-index range)
length (.-length range)
delta (.import js/Quill "delta")]
(cb (fn [url]
(.updateContents #this
(.insert
(.delete
(.retain (new delta) index)
length)
#js {:image url}))))))
(defn editor [{:keys [id value selection on-change image-handler]}]
(let [this (r/atom nil)
get-value #(aget #this "container" "firstChild" "innerHTML")
string-id (if (keyword? id) (name id) id) ]
(r/create-class
{:component-did-mount
(fn [component]
(reset! this
(js/Quill.
(aget (.-children (r/dom-node component)) 1)
#js {:modules #js {:toolbar (aget (.-children (r/dom-node component)) 0)}
:theme "snow"
:scrollingContainer (str "quill-wrapper-" string-id)
:placeholder "Compose an epic..."}))
(.on #this "text-change"
(fn [delta old-delta source]
(on-change source (get-value))))
; FYI this is another area I had trouble. I got around it using
; get and set in the goog.object
(let [toolbar (.getModule #this "toolbar")
handlers (goog.object/get toolbar "handlers")]
(goog.object/set handlers "image" #(file-recieve-handler this image-handler)))
(if (= selection nil)
(.setSelection #this nil)
(.setSelection #this (first selection) (second selection) "api")))
:component-will-receive-props
(fn [component next-props]
(if
(or
(not= (:value (second next-props)) (get-value))
(not= (:id (r/props component)) (:id (second next-props))))
(do
(if (= selection nil)
(.setSelection #this nil)
(.setSelection #this (first selection) (second selection) "api"))
(.pasteHTML #this (:value (second next-props))))))
:display-name (str "quill-editor-" string-id)
:reagent-render
(fn []
[:div {:id (str "quill-wrapper-" string-id) :class "quill-wrapper"}
[quill-toolbar string-id]
[:div {:id (str "quill-editor-" string-id)
:class "quill-editor"
:dangerouslySetInnerHTML {:__html value}}]])})))
(defn display-area [{:keys [id content]}]
(let [this (r/atom nil)]
(r/create-class
{:component-did-mount
(fn [component]
(reset! this (js/Quill. (r/dom-node component)
#js {:theme "snow"
:modules #js {:toolbar false}
:placeholder ""}))
(.disable #this))
:component-will-receive-props
(fn [component next-props]
(.pasteHTML #this (:content (second next-props))))
:display-name (str "quill-display-area-" id)
:reagent-render
(fn []
[:div {:id (str "quill-display-area-" id)
:class "quill-display-area"
:dangerouslySetInnerHTML {:__html content}}])})))
You can turn on externs inference warnings and the compiler will tell you about things that are likely to rename.
;; in the actual ns
(set! *warn-on-infer* true)
;; in the build config compiler options
:infer-externs true
See https://clojurescript.org/guides/externs#externs-inference
To help debug issues with renaming you can turn on :pseudo-names true in the compiler options. That'll make it easier to figure out which methods get renamed and may need a ^js typehint or manual externs.
I used the type hints as #thomas-heller suggested and it worked. I broke the original function into two separate functions. Here it is re-written:
(defn add-image [^js/Quill quill ^js/Quill.imports.delta delta index length url]
(.updateContents quill
(.insert
(.delete
(.retain delta index)
length)
#js {:image url})))
;https://github.com/quilljs/quill/pull/995/files#diff-6dafc0fe6b5e9aed0859eef541e68372
(defn file-recieve-handler [^js/Quill quill cb]
(let [range (.getSelection quill true)
index (.-index range)
length (.-length range)
delta (new (.-delta (.-imports js/Quill)))]
(cb (fn [url]
(add-image quill delta index length url)))))

How to update an element in vector in atom state

I'm trying to create kind of todo list with ClojureScript and reagent framework. I defined app state as atom:
(def app-state
(r/atom
{:count 3
:todolist
[{:id 0 :text "Start learning mindcontrol" :finished true}
{:id 1 :text "Read a book 'Debugging JS in IE11 without pain'" :finished false}
{:id 2 :text "Become invisible for a while" :finished false}]}))
Have a function to update todo list:
(defn update-todolist [f & args]
(apply swap! app-state update-in [:todolist] f args))
And function toggle todo:
(defn toggle-todo [todo]
(update-todolist update-in [2] assoc :finished true))
Here I'm updating vector element directly by its index right now.
I'm rendering every item with this function:
(defn item [todo]
^{:key (:id todo)}
[:div
[:span {:class "item-text"} (:text todo)]
[:i {:class (str "ti-check " (if (:finished todo) "checked" "unchecked"))
:on-click #(toggle-todo (assoc todo :finished true))}]])
Here I'm passing updated todo but it's not correct to pass always true. Probably it would be enough to pass its index and it will solve my problem, but I have no idea how to do this.
(def app-state
(r/atom
{:count 3
:todolist
[{:id 0 :text "Start learning mindcontrol" :finished true}
{:id 1 :text "Read a book 'Debugging JS in IE11 without pain'" :finished false}
{:id 2 :text "Become invisible for a while" :finished false}]}))
(defn update-todolist [f & args]
(apply swap! app-state update-in [:todolist] f args))
(defn toggle-todo [todo]
(swap! app-state update-in [:todolist (:id todo) :finished] not))
(defn item [todo]
^{:key (:id todo)}
[:div
[:span {:class "item-text"} (:text todo)]
[:i {:class (str "ti-check " (if (:finished todo) "checked" "unchecked"))
:on-click #(toggle-todo todo)}]])
To toggle the value of the :finished key, just use not:
(swap! app-state update-in [:todolist 2 :finished] not) =>
{:count 3,
:todolist
[{:id 0, :text "Start learning mindcontrol",
:finished true}
{:id 1, :text "Read a book 'Debugging JS in IE11 without pain'",
:finished false}
{:id 2, :text "Become invisible for a while",
:finished true}]}
However, this does not tell you how the index 2 corresponds with the map that has :id 2 inside it.

Reagent component doesn't update

The 'add' and 'remove' items of shoppinglist ratom updates the 'shopping-list' component, but the 'update' doesn't.
I used cljs REPL for updating the shoppinglist ratom.
add:
shopping.app=> (swap! shoppinglist assoc 3 {:id 3, :name "Coke", :price 25})
WARNING: Use of undeclared Var shopping.app/shoppinglist at line 1 <cljs repl>
{1 {:id 1, :name "Bread", :price 23}, 2 {:id 2, :name "Milk", :price 12}, 3 {:id 3, :name "Coke", :price 25}}
remove:
shopping.app=> (swap! shoppinglist dissoc 3)
WARNING: Use of undeclared Var shopping.app/shoppinglist at line 1 <cljs repl>
{1 {:id 1, :name "Bread", :price 20}, 2 {:id 2, :name "Milk", :price 12}}
update:
shopping.app=> (swap! shoppinglist assoc 2 {:id 2, :name "Milk", :price 8})
WARNING: Use of undeclared Var shopping.app/shoppinglist at line 1 <cljs repl>
{1 {:id 1, :name "Bread", :price 20}, 2 {:id 2, :name "Milk", **:price 8**}}
shopping.app=>
The shoppinglist ratom is updated, I checked it in the REPL, but the component is not updated.
(ns shopping.app
(:require [reagent.core :as r]))
(defonce shoppinglist (r/atom (sorted-map
1 {:id 1 :name "Bread" :price 20},
2 {:id 2 :name "Milk" :price 12})))
(defn shopping-item [item]
(let [{:keys [id name price]} item]
(fn []
[:div
[:label id]
[:label (str " | " name)]
[:label (str " | " price)]])))
(defn shopping-list []
[:div.container
(for [item (vals #shoppinglist)]
^{:key (:id item)} [:div
[shopping-item item]])])
(defn init
"Initialize components."
[]
(let [container (.getElementById js/document "container")]
(r/render-component
[shopping-list]
container)))
EDITED ==================================
I found a good overview about the component designs here https://github.com/reagent-project/reagent/blob/master/docs/CreatingReagentComponents.md
Form-2: I didn't put the local state into my example, but my real app contains local stater ratom. According this, I had to put the same parameters for the embedded render functions what the component function has. I modified my example, added local state ratom and params the render function and it works well.
(ns shopping.app
(:require [reagent.core :as r]))
(defonce shoppinglist (r/atom (sorted-map
1 {:id 1 :name "Bread" :price 20},
2 {:id 2 :name "Milk" :price 12})))
(defn shopping-item [{:keys [id name price]}]
(let [loacalstate (r/atom true)]
(fn [{:keys [id name price]}]
[:div
[:label id]
[:label (str " | " name)]
[:label (str " | " price)]])))
(defn shopping-list []
[:div.container
(for [item (vals #shoppinglist)]
^{:key (:id item)} [:div
[shopping-item item]])])
(defn init
"Initialize components."
[]
(let [container (.getElementById js/document "container")]
(r/render-component
[shopping-list]
container)))
Remove the inner no-arg wrapping function in shopping-item. It is unnecessary, but will prevent re-rendering of existing items, because the no-arg function doesn't see the changed argument.
So:
(defn shopping-item [item]
(let [{:keys [id name price]} item]
[:div
[:label id]
[:label (str " | " name)]
[:label (str " | " price)]]))
or
(defn shopping-item [{:keys [id name price]}]
[:div
[:label id]
[:label (str " | " name)]
[:label (str " | " price)]])
For more information, check out this documentation of form-2 components:
https://github.com/reagent-project/reagent/blob/master/doc/CreatingReagentComponents.md#form-2--a-function-returning-a-function

Om Next subquery doesn't have effect on sub component props

Reading this Om Next tutorial page Components, Identity & Normalization, I thought the subquery from the subcomponent (Person component) is used to populate the Person's props. But changing the query from
'[:name :points :age]
to
'[]
doesn't break the app. Could you help me understand how the parser invokes read methods from these component/query tree.
The entire code from the page is below.
(def init-data
{:list/one [{:name "John" :points 0}
{:name "Mary" :points 0}
{:name "Bob" :points 0}]
:list/two [{:name "Mary" :points 0 :age 27}
{:name "Gwen" :points 0}
{:name "Jeff" :points 0}]})
;; -----------------------------------------------------------------------------
;; Parsing
(defmulti read om/dispatch)
(defn get-people [state key]
(let [st #state]
(into [] (map #(get-in st %)) (get st key))))
(defmethod read :list/one
[{:keys [state] :as env} key params]
{:value (get-people state key)})
(defmethod read :list/two
[{:keys [state] :as env} key params]
{:value (get-people state key)})
(defmulti mutate om/dispatch)
(defmethod mutate 'points/increment
[{:keys [state]} _ {:keys [name]}]
{:action
(fn []
(swap! state update-in
[:person/by-name name :points]
inc))})
(defmethod mutate 'points/decrement
[{:keys [state]} _ {:keys [name]}]
{:action
(fn []
(swap! state update-in
[:person/by-name name :points]
#(let [n (dec %)] (if (neg? n) 0 n))))})
;; -----------------------------------------------------------------------------
;; Components
(defui Person
static om/Ident
(ident [this {:keys [name]}]
[:person/by-name name])
static om/IQuery
(query [this]
'[])
Object
(render [this]
(println "Render Person" (-> this om/props :name))
(let [{:keys [points name age] :as props} (om/props this)]
(dom/li nil
(dom/label nil (str name ", points: " points ", age: " age))
(dom/button
#js {:onClick
(fn [e]
(om/transact! this
`[(points/increment ~props)]))}
"+")
(dom/button
#js {:onClick
(fn [e]
(om/transact! this
`[(points/decrement ~props)]))}
"-")))))
(def person (om/factory Person {:keyfn :name}))
(defui ListView
Object
(render [this]
(println "Render ListView" (-> this om/path first))
(let [list (om/props this)]
(apply dom/ul nil
(map person list)))))
(def list-view (om/factory ListView))
(defui RootView
static om/IQuery
(query [this]
(let [subquery (om/get-query Person)]
`[{:list/one ~subquery} {:list/two ~subquery}]))
Object
(render [this]
(println "Render RootView")
(let [{:keys [list/one list/two]} (om/props this)]
(apply dom/div nil
[(dom/h2 nil "List A")
(list-view one)
(dom/h2 nil "List B")
(list-view two)]))))
(def reconciler
(om/reconciler
{:state init-data
:parser (om/parser {:read read :mutate mutate})}))
(om/add-root! reconciler
RootView (gdom/getElement "app"))
From reading this now I know that only the top level queries are processed by the parser. And you are responsible for providing your own reads for subqueries by accessing (:query env).

om.next mutate of other-component state not causing other-component to re-render

I am updating state in one of my mutations, and a piece of it is not used by this component, but is by another one. When I do the mutate I see the that the app-state is updated in the repl, and if I cause the component to re-render for other reasons, it will show correctly, but I can not get the mutate to schedule a re-render of the second component. In the example below clicking on a button should decrement the value near the color name in the second list, but it does not.
There is some examples showing using :value [k k] in the mutate return, but those throw an error, must be out of date tutorials, as the current format is :value {:keys [...]}, so says the code and some tutorials . However I can't find any part of om.next actually USING :keys as a keyword that isn't a destructure operation (so not using :keys as an actual keyword, but it is a common word so I may have missed one somewhere)
In the repl I see this for the app-state:
=> (om/app-state reconciler)
#object [cljs.core.Atom {:val
{:tiles [[:tile/by-pos "a7"]
[:tile/by-pos "a9"]
[:tile/by-pos "a11"]],
:inventory [[:inv/by-color "red"]
[:inv/by-color "blue"]
[:inv/by-color "green"]],
:tile/by-pos {"a7" {:pos "a7", :color nil},
"a9" {:pos "a9", :color nil},
"a11" {:pos "a11", :color nil}},
:inv/by-color {"red" {:color "red", :remaining 2},
"blue" {:color "blue", :remaining 1},
"green" {:color "green", :remaining 1}}}}]
What am I missing?
(ns omnexttest.core
(:require [goog.dom :as gdom]
[om.next :as om :refer-macros [defui]]
[om.dom :as dom]))
(defmulti read om/dispatch)
(defmethod read :default
[{:keys [state] :as env} key params]
(let [st #state ]
(if-let [[_ value] (find st key)]
{:value value}
{:value :not-found})))
(defmethod read :tiles
[{:keys [state] :as env} key params]
{:value (into [] (map #(get-in #state %) (get #state key))) })
(defmethod read :inventory
[{:keys [state] :as env} key params]
{:value (into [] (map #(get-in #state %) (get #state key))) })
(defmulti mutate om/dispatch)
(defmethod mutate 'draw/edit-edge
[{:keys [state] :as env} _ {:keys [this pos color]}]
{:value {:keys [[:inv/by-color color :remaining]]}
:action (fn [] (do
(swap! state assoc-in [:tile/by-pos pos :color] color )
(swap! state update-in [:inv/by-color color :remaining] dec)))})
(defn hex-color
[ this pos color ]
(om/transact! this `[(draw/edit-edge ~{:this this :pos pos :color color})]))
(defui TileView
static om/Ident
(ident [this {:keys [pos]}] [:tile/by-pos pos])
static om/IQuery
(query [this] '[:pos :color])
Object
(render [this]
(let [{:keys [pos color] :as props} (om/props this)]
(dom/li nil
(str pos " " color)
(for [color ["red" "green" "blue"]]
(dom/button #js { :onClick (fn [e] (hex-color this pos color)) }
color))))))
(def tile-view (om/factory TileView {:keyfn :pos}))
(defui InvView
static om/Ident
(ident [this {:keys [color]}] [:inv/by-color color])
static om/IQuery
(query [this] '[:color :remaining])
Object
(render [this]
(let [{:keys [color remaining] :as props} (om/props this) ]
(dom/li nil (str color " " remaining)))))
(def inv-view (om/factory InvView {:keyfn :color}))
(def app-state {
:tiles [{ :pos "a7" :color nil }
{ :pos "a9" :color nil }
{ :pos "a11" :color nil }
]
:inventory [{ :color "red" :remaining 2}
{ :color "blue" :remaining 1}
{ :color "green" :remaining 1}]
})
(defui MapView
static om/IQuery
(query [this]
[{:tiles (om/get-query TileView)}
{:inventory (om/get-query InvView) }])
Object
(render [this]
(let [tiles (-> this om/props :tiles)
inv (-> this om/props :inventory) ]
(dom/div nil
(dom/ul nil
(mapv tile-view tiles))
(dom/ul nil
(mapv inv-view inv))))))
(def reconciler
(om/reconciler
{:state app-state
:parser (om/parser {:read read :mutate mutate})}))
(om/add-root! reconciler
MapView (gdom/getElement "map"))
(defn on-js-reload []
;; optionally touch your app-state to force rerendering depending on
;; your application
;; (swap! app-state update-in [:__figwheel_counter] inc)
)
The this that is passed into om/transact! is important for re-rendering, so here if this was for a MapView component then all three components would be re-rendered. You can have the function in MapView (thus using MapView's this) but call it from TileView. In TileView's render you need something like this:
{:keys [click-cb-fn]} (om/get-computed this)
When you call om/transact! re-rendering is done down from the component you pass as first argument - this. Thus, to take this to its extreme, you'll never have re-rendering problems if all om/transacts!s are done from the root component, and all functions are passed down via computed props.
But you don't have to pass functions down. An alternative is to keep them at the same component where the firing button is, and instead pass down (again via computed props) the parent component's this. All that matters is what component the first argument to om/transact! is - call om/transact! from where ever you like.
Follow on reads are another thing to be considered when thinking about re-rendering, but not for the example you gave - they are best considered when the component you need to be re-rendered is in a different subbranch of the render tree, where using a common root's this would not be practical.
Another thing to note is that a mutate's value is 'just for documentation'. So whatever you put there will have no effect.