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
Related
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.
I am currently learning reagent with secretary as its route. I find that I can use query-params to get a hash-map of all parameters with question mark (?) like ?name=Daniel
(ns feampersanda.core
(:require-macros [secretary.core :refer [defroute]])
(:import goog.History)
(:require
[secretary.core :as secretary]
[goog.events :as events]
[goog.history.EventType :as EventType]
[reagent.core :as r]))
;; ------------------------------
;; States
;; page --> is occupied by page state
(def app-state (r/atom {:params {}}))
;; ------------------------------
;; History
(defn hook-browser-navigation! []
(doto (History.)
(events/listen
EventType/NAVIGATE
(fn [event]
(secretary/dispatch! (.-token event))))
(.setEnabled true)))
;; -------------------------
;; Views
;; -------------------------
;; Parameters
(defn update-query-params! [query-params]
(do
(js/console.log (str query-params))
(swap! app-state assoc-in [:params :query] query-params))
)
;; -------------------------
;; Routing Config
(defn app-routes []
(secretary/set-config! :prefix "#")
(defroute "/" [query-params]
(do
(update-query-params! query-params)
(swap! app-state assoc :page :home)))
(defroute "/about/:id" [id query-params]
(do
(js/console.log id)
(update-query-params! query-params)
(swap! app-state assoc :page :about)))
(hook-browser-navigation!))
(defmulti current-page #(#app-state :page))
(defmethod current-page :home []
[:div [:h1 (str "Home Page")]
[:a {:href "#/about"} "about page"]
[:br]
[:a {:href "#/about"} (str (:count #app-state))]
])
(defmethod current-page :about []
[:div [:h1 "About Page"]
[:a {:href "#/"} (str "home page" " --> "
(:yes (:query (:params #app-state)))
)]])
(defmethod current-page :default []
[:div
[:p "404"]
])
;; -------------------------
;; Initialize app
(defn mount-root []
(app-routes)
(r/render [current-page] (.getElementById js/document "app")))
(defn init! []
(mount-root))
I don't know how to pass the id parameter to a defmethod, so I want it to be saved inside an atom, so I wonder how to get hash-map which is include all of the named parameters like http://0.0.0.0:3449/#/about/black/12 to {:path "black" :id "12"}
One solution would be to use cemerick's URL library
(require '[cemerick.url :as url])
(keys (:query (url/url (-> js/window .-location .-href))))
https://github.com/cemerick/url
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).
Consider the following hypothetical, simplified clojurescript snippets:
(def cat (r/atom [{:id 0 :data {:text "ROOT" :test 17} :prev nil :par nil}
{:id 1 :data {:text "Objects" :test 27} :prev nil :par 0}
{:id 2 :data {:text "Version" :test 37} :prev nil :par 1}]))
(defn categorymanager [s]
[:div
[:> Reactable.Table
{:data (clj->js
s
)}
]
]
)
(defn content []
(fn []
[:div
[:h1 "Test"]
(categorymanager (select [ALL :data] (t/tree-visitor #cat)))
[re-com/button :label "Do not click!"]
]
))
The content function prepares a Reagent component. The code snippets work as expected. ( The 'select' function is part of the Specter library. )
I would like to add the minimum re-frame code such that when the cat atom is changed, for example with a function from within the REPL, the React.js component in the browser is changed. I know the theory about re-frame subscriptions and handlers, but only in theory since I haven't been able to get it to work in such a minimal example as this. How is it done? How to push changes to a Reagent component with Re-frame subscriptions and handlers?
You should first initialize re-frame app-db by dispatching some kind of initializer. Re-frame works with it's internal single app-db. You can dispatch with dispatch-sync before mounting top React component, this way app will be rendered once it's initialized.
For your specific example it would be something like this (not tested at all):
; Initialize our db here. This one should be called with (re-frame/dispatch-sync [:initialize]) before rendering application.
(re-frame/register-handler
:initialize
(fn [_]
{:cats [{:id 0 :data {:text "ROOT" :test 17} :prev nil :par nil}
{:id 1 :data {:text "Objects" :test 27} :prev nil :par 0}
{:id 2 :data {:text "Version" :test 37} :prev nil :par 1}]}))
; This one returns reaction with relevant cat list.
(re-frame/register-sub
:cats
(fn [db]
(reaction
(get #db :cats))))
(defn categorymanager [s]
[:div
[:> Reactable.Table
{:data (clj->js
s)}]])
(defn content []
; Here you subscribe to cat list. Once cat list is changed, component is rerendered.
(let [cats (re-frame/subscribe [:cats])]
(fn []
[:div]
[:h1 "Test"]
(categorymanager (select [ALL :data] (t/tree-visitor #cats)))
[re-com/button :label "Do not click!"])))
What is the appropriate json way to save and reload enlive's html-resource outputs.
The following procedure does not preserve the data structure (note that I ask json/read-str to map keys to symbols):
(require net.cgrand.enlive-html :as html)
(require clojure.data.json :as json)
(def craig-home
(html/html-resource (java.net.URL. "http://www.craigslist.org/about/sites")))
(spit "./data/test_json_flow.json" (json/write-str craig-home))
(def craig-reloaded
(json/read-str (slurp "./data/test_json_flow.json") :key-fn keyword))
(defn count-nodes [page] (count (html/select page [:div.box :h4])))
(println (count-nodes craig-home)) ;; => 140
(println (count-nodes craig-reloaded)) ;; => 0
Thanks.
UPDATE
To address Mark Fischer's comment I post a different code that address html/select instead of html/html-resource
(def craig-home
(html/html-resource (java.net.URL. "http://www.craigslist.org/about/sites")))
(def craig-boxes (html/select craig-home [:div.box]))
(count (html/select craig-boxes [:h4])) ;; => 140
(spit "./data/test_json_flow.json" (json/write-str craig-boxes))
(def craig-boxes-reloaded
(json/read-str (slurp "./data/test_json_flow.json") :key-fn keyword))
(count (html/select craig-boxes-reloaded [:h4])) ;; => 0
A simpler approach would be to write/read using Clojure edn:
(require '[net.cgrand.enlive-html :as html])
(require '[clojure.data.json :as json])
(def craig-home (html/html-resource (java.net.URL. "http://www.craigslist.org/about/sites")))
(spit "./data/test_json_flow.json" (pr-str craig-home))
(def craig-reloaded
(clojure.edn/read-string (slurp "./data/test_json_flow.json")))
(defn count-nodes [page] (count (html/select page [:div.box :h4])))
(println (count-nodes craig-home)) ;=>140
(println (count-nodes craig-reloaded)) ;=>140
Enlive expects the tag name value also to be a keyword and will not find a node if the tag name value is a string (which is what json/write-str and json/read-str converts keywords to).
(json/write-str '({:tag :h4, :attrs nil, :content ("Illinois")}))
;=> "[{\"tag\":\"h4,\",\"attrs\":null,\"content\":[\"Illinois\"]}]"
(json/read-str (json/write-str '({:tag :h4, :attrs nil, :content ("Illinois")})) :key-fn keyword)
;=> [{:tag "h4", :attrs nil, :content ["Illinois"]}]
(pr-str '({:tag :h4 :attrs nil :content ("Illinois")}))
;=> "({:tag :h4, :attrs nil, :content (\"Illinois\")})"
(clojure.edn/read-string (pr-str '({:tag :h4, :attrs nil, :content ("Illinois")})))
;=> ({:tag :h4, :attrs nil, :content ("Illinois")})
If you must use json then you can use the following to convert the :tag values to keywords:
(clojure.walk/postwalk #(if-let [v (and (map? %) (:tag %))]
(assoc % :tag (keyword v)) %)
craig-reloaded)