I have a select component which shows a label and the associated options. There is a separate 'language' select which should choose the language of the display. On change it updates both #language which choses the label language and #search-language-options which provides the options to the select.
The label updates as expected, but the options list does not - it remains as originally initialised.
;; search-languages -> vector
;; return the languages referenced in the data for options use
(defn search-languages []
(let [options (list (tr {:dict ld} [#language :en] [:choice-any]))]
(if (= #language :or) (concat options (vec (set (map (partial get-field "lang") parsed-json))))
(concat options (vec (set (map (partial get-field "langTranslated") parsed-json)))))))
(def search-language-options (r/atom (search-languages)))
;; filter component -> component
;; create a select
(defn select-filter-component [label value options]
[:div {:class "form-group"}
[:label {:class "control-label" :for label} label]
[:select {:id label :class "form-control" :value #value :on-change #(reset! value (-> % .-target .-value))}
(for [opt options]
^{:key opt} [:option {:value opt} opt])]])
;; lang-select -> component
;; choose display language
(defn lang-select []
[:div {:class "form-control" }
[:select {:id :en :value #language :on-change (fn [e]
(reset! language (.. e -target -value))
(reset! search-language-options (search-languages)))
}
^{:key :en} [:option {:value :en} "English"]
^{:key :fr} [:option {:value :fr} "Français"]
^{:key :es} [:option {:value :es} "Español"]
^{:key :or} [:option {:value :or} "Original language"]
]])
;; filter form
;; filter the results
(defn filter-form []
[:form {:class "form-inline"}
[search-filter-component (tr {:dict ld} [#language :en] [:search] ) search-text]
[select-filter-component (tr {:dict ld} [#language :en] [:language] ) search-language #search-language-options]
[select-filter-component (tr {:dict ld} [#language :en] [:gender] ) gender ["Any" "M" "F"]]
[select-filter-component (tr {:dict ld} [#language :en] [:continent/title] ) continent ["Any" "Europe"]]
[select-filter-component (tr {:dict ld} [#language :en] [:country] ) placeTranslated ["Any" "Austria" "Switzerland" "Germany"]]
[select-filter-component (tr {:dict ld} [#language :en] [:literaryForm] ) literaryForm ["Any" "Drama" "Poetry" "Prose: fiction" "Prose: non-fiction" ]]
[select-filter-component (tr {:dict ld} [#language :en] [:genre] ) genre ["Any" "Short story" "Novella" "Graphic Novel"]]
[reset-button]]
)
Do you mean that the [:option...] elements in select-filter-component don't change? If that's what you mean, it is hard to tell from the code posted why they don't change because those options are rendered based on the props passed into select-filter-component, and you haven't shown how you are calling select-filter-component. Specifically this code is completely dependent on the options prop passed in:
(for [opt options]
^{:key opt} [:option {:value opt} opt])
(Side note: though it doesn't matter here, note that for is lazy, so if you ever reference an atom in a for note that you'll have to wrap it in a doall or the atom won't be referenced during rendering and your component won't update when the atom updates.)
If you mean that selecting different options with the mouse doesn't work, then that is because you have commented out the on-change handler.
Related
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)))))
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.
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).
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.
I'm trying to understand how to read state from a text box in om.next. As I understand it, we are no longer bound/supposed to use core.async.
As a small example, consider writing in a textbox and binding it to a paragraph element, so that the text you enter automatically appears on the screen.
(def app-state (atom {:input-text "starting text"}))
(defn read-fn
[{:keys [state] :as env} key params]
(let [st #state]
(if-let [[_ v] (find st key)]
{:value v}
{:value :not-found})))
(defn mutate-fn
[{:keys [state] :as env} key {:keys [mytext]}]
(if (= 'update-text key)
{:value {:keys [:input-text]}
:action
(fn []
(swap! state assoc :input-text mytext))}
{:value :not-found}))
(defui RootView
static om/IQuery
(query [_]
[:input-text])
Object
(render [_]
(let [{:keys [input-text]} (om/props _)]
(dom/div nil
(dom/input
#js {:id "mybox"
:type "text"
:value input-text
:onChange #(om/transact! _ '[(update-text {:mytext (.-value (gdom/getElement "mybox"))})])
})
(dom/p nil input-text)))))
This doesn't work.
When firing the onChange event in the input form, the quoted expression does not grab the text from the box.
The first mutation fires and updates, but then subsequent mutations are not fired. Even though the state doesn't changed, should the query read the string from app-state and force the text to be the same?
I would make the :onChange event look like this:
:onChange (fn (_)
(let [v (.-value (gdom/getElement "mybox"))]
#(om/transact! this `[(update-text {:mytext ~v})])))
Here the value v will actually be going through. But also om/transact! needs either a component or the reconciler to be passed as its first parameter. Here I'm passing in this which will be the root component.